| #!/usr/bin/python3 |
| # Copyright 2021 The Android Open Source Project |
| # |
| # Licensed under the Apache License, Version 2.0 (the "License"); |
| # you may not use this file except in compliance with the License. |
| # You may obtain a copy of the License at |
| # |
| # http://www.apache.org/licenses/LICENSE-2.0 |
| # |
| # Unless required by applicable law or agreed to in writing, software |
| # distributed under the License is distributed on an "AS IS" BASIS, |
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| # See the License for the specific language governing permissions and |
| # limitations under the License. |
| |
| import argparse |
| import datetime |
| import os |
| import os.path |
| import re |
| import subprocess |
| import sys |
| """ |
| Helper script for importing a new snapshot of the official Wayland sources. |
| |
| Usage: ./import_official_snapshot.py 1.18.0 |
| """ |
| |
| |
| def git(cmd, check=True): |
| return subprocess.run(['git'] + cmd, |
| capture_output=True, |
| check=check, |
| text=True) |
| |
| |
| def git_get_hash(commit): |
| return git(['show-ref', '--head', '--hash', commit]).stdout.strip() |
| |
| |
| def git_is_ref(ref): |
| return git( |
| ['show-ref', '--head', '--hash', '--verify', f'refs/heads/{ref}'], |
| check=False).returncode == 0 |
| |
| |
| def get_git_files(version): |
| return set( |
| git(['ls-tree', '-r', '--name-only', |
| f'{version}^{{tree}}']).stdout.split()) |
| |
| |
| def assert_no_uncommitted_changes(): |
| r = git(['diff-files', '--quiet', '--ignore-submodules'], check=False) |
| if r.returncode: |
| sys.exit('Error: Your tree is dirty') |
| |
| r = git( |
| ['diff-index', '--quiet', '--ignore-submodules', '--cached', 'HEAD'], |
| check=False) |
| if r.returncode: |
| sys.exit('Error: You have staged changes') |
| |
| |
| def metadata_read_current_version(): |
| with open('METADATA', 'rt') as metadata_file: |
| metadata = metadata_file.read() |
| |
| match = re.search(r'version: "([^"]*)"', metadata) |
| if not match: |
| sys.exit('Error: Unable to determine current version from METADATA') |
| return match.group(1) |
| |
| |
| def metadata_read_git_url(): |
| with open('METADATA', 'rt') as metadata_file: |
| metadata = metadata_file.read() |
| |
| match = re.search(r'url\s*{\s*type:\s*GIT\s*value:\s*"([^"]*)"\s*}', |
| metadata) |
| if not match: |
| sys.exit('Error: Unable to determine GIT url from METADATA') |
| return match.group(1) |
| |
| |
| def setup_and_update_official_source_remote(official_source_git_url): |
| r = git(['remote', 'get-url', 'official-source'], check=False) |
| if r.returncode or r.stdout != official_source_git_url: |
| # Not configured as expected. |
| print( |
| f' Configuring official-source remote {official_source_git_url}') |
| git(['remote', 'remove', 'official-source'], check=False) |
| git(['remote', 'add', 'official-source', official_source_git_url]) |
| |
| print(' Syncing official-source') |
| git(['remote', 'update', 'official-source']) |
| |
| |
| def get_local_files(): |
| result = [] |
| for root, dirs, files in os.walk('.'): |
| # Don't include ./.git |
| if root == '.' and '.git' in dirs: |
| dirs.remove('.git') |
| for name in files: |
| result.append(os.path.join(root, name)[2:]) |
| return result |
| |
| |
| def determine_files_to_preserve(current_version): |
| local_files = set(get_local_files()) |
| |
| current_official_files = get_git_files(current_version) |
| |
| android_added_files = local_files - current_official_files |
| |
| return android_added_files |
| |
| |
| def update_metadata_version_and_import_date(version): |
| now = datetime.datetime.now() |
| |
| with open('METADATA', 'rt') as metadata_file: |
| metadata = metadata_file.read() |
| |
| metadata = re.sub(r'version: "[^"]*"', f'version: "{version}"', metadata) |
| metadata = re.sub( |
| r'last_upgrade_date {[^}]*}', |
| (f'last_upgrade_date {{ year: {now.year} month: {now.month} ' |
| f'day: {now.day} }}'), metadata) |
| |
| with open('METADATA', 'wt') as metadata_file: |
| metadata_file.write(metadata) |
| |
| |
| def configure_wayland_version_header(version): |
| with open('./src/wayland-version.h.in', 'rt') as template_file: |
| content = template_file.read() |
| |
| (major, minor, micro) = version.split('.') |
| |
| content = re.sub(r'@WAYLAND_VERSION_MAJOR@', major, content) |
| content = re.sub(r'@WAYLAND_VERSION_MINOR@', minor, content) |
| content = re.sub(r'@WAYLAND_VERSION_MICRO@', micro, content) |
| content = re.sub(r'@WAYLAND_VERSION@', version, content) |
| |
| with open('./src/wayland-version.h', 'wt') as version_header: |
| version_header.write(content) |
| |
| # wayland-version.h is in .gitignore, so we explicitly have to force-add it. |
| git(['add', '-f', './src/wayland-version.h']) |
| |
| |
| def import_sources(version, preserve_files, update_metdata=True): |
| start_hash = git_get_hash('HEAD') |
| |
| # Use `git-read-tree` to start with a pure copy of the imported version |
| git(['read-tree', '-m', '-u', f'{version}^{{tree}}']) |
| |
| git(['commit', '-m', f'To squash: Clean import of {version}']) |
| |
| print(' Adding Android metadata') |
| |
| # Restore the needed Android files |
| git(['restore', '--staged', '--worktree', '--source', start_hash] + |
| list(sorted(preserve_files))) |
| |
| if update_metdata: |
| update_metadata_version_and_import_date(version) |
| configure_wayland_version_header(version) |
| |
| git(['commit', '-a', '-m', f'To squash: Update versions {version}']) |
| |
| |
| def apply_and_reexport_patches(version, patches, use_cherry_pick=False): |
| if not patches: |
| return |
| |
| print(f' Applying {len(patches)} Android patches') |
| |
| try: |
| if use_cherry_pick: |
| git(['cherry-pick'] + patches) |
| else: |
| git(['am'] + patches) |
| except subprocess.CalledProcessError as e: |
| if 'patch failed' not in e.stderr: |
| raise |
| # Print out the captured error mess |
| sys.stderr.write(f''' |
| Failure applying patches to Wayland {version} via: |
| {e.cmd} |
| |
| Once the patches have been resolved, please re-export the patches with: |
| |
| git rm patches/*.diff |
| git format-patch HEAD~{len(patches)}..HEAD --no-stat --no-signature \\ |
| --numbered --zero-commit --suffix=.diff --output-directory patches |
| |
| ... and also add them to the final squashed commit. |
| '''.strip()) |
| |
| sys.stdout.write(e.stdout) |
| sys.exit(e.stderr) |
| |
| patch_hashes = list( |
| reversed( |
| git(['log', f'-{len(patches)}', |
| '--pretty=format:%H']).stdout.split())) |
| |
| # Clean out the existing patches |
| git(['rm', 'patches/*.diff']) |
| |
| # Re-export the patches, omitting information that might change |
| git([ |
| 'format-patch', f'HEAD~{len(patches)}..HEAD', '--no-stat', |
| '--no-signature', '--numbered', '--zero-commit', '--suffix=.diff', |
| '--output-directory', 'patches' |
| ]) |
| |
| # Add back all the exported patches |
| git(['add', 'patches/*.diff']) |
| |
| # Create a commit for the exported patches if there are any differences. |
| r = git(['diff-files', '--quiet', '--ignore-submodules'], check=False) |
| if r.returncode: |
| git(['commit', '-a', '-m', f'To squash: Update patches for {version}']) |
| |
| return patch_hashes |
| |
| |
| def main(): |
| parser = argparse.ArgumentParser(description=( |
| "Helper script for importing a snapshot of the Wayland sources.")) |
| parser.add_argument('version', |
| nargs='?', |
| default=None, |
| help='The official version to import') |
| parser.add_argument( |
| '--no-validate-existing', |
| dest='validate_existing', |
| default=True, |
| action='store_false', |
| help='Whether to validate the current tree against upstream + patches') |
| parser.add_argument('--no-squash', |
| dest='squash', |
| default=True, |
| action='store_false', |
| help='Whether to squash the import to a single commit') |
| args = parser.parse_args() |
| |
| print( |
| f'Preparing to importing Wayland core sources version {args.version}') |
| |
| assert_no_uncommitted_changes() |
| |
| official_source_git_url = metadata_read_git_url() |
| current_version = metadata_read_current_version() |
| |
| setup_and_update_official_source_remote(official_source_git_url) |
| |
| # Get the list of Android added files to preserve |
| preserve_files = determine_files_to_preserve(current_version) |
| |
| # Filter the list to get all patches that we will need to apply |
| patch_files = sorted(path for path in preserve_files |
| if path.startswith('patches/')) |
| |
| # Detect any add/add conflicts before we begin |
| new_files = get_git_files(args.version or current_version) |
| add_add_conflicts = preserve_files.intersection(new_files) |
| if add_add_conflicts: |
| sys.exit(f''' |
| Error: The new version of Wayland adds files that are also added for Android: |
| {add_add_conflicts} |
| '''.strip()) |
| |
| import_branch_name = f'import_{args.version}' if args.version else None |
| |
| if import_branch_name and git_is_ref(import_branch_name): |
| sys.exit(f''' |
| Error: Branch name {import_branch_name} already exists. Please delete or rename. |
| '''.strip()) |
| |
| initial_commit_hash = git_get_hash('HEAD') |
| |
| # Begin a branch for the version import, if a new version is being imported |
| if import_branch_name: |
| git(['checkout', '-b', import_branch_name]) |
| git([ |
| 'commit', '--allow-empty', '-m', |
| f'Update to Wayland {args.version}' |
| ]) |
| |
| patch_hashes = None |
| |
| if args.validate_existing: |
| print(f'Importing {current_version} to validate all current fixups') |
| import_sources(current_version, preserve_files, update_metdata=False) |
| patch_hashes = apply_and_reexport_patches(current_version, patch_files) |
| |
| r = git( |
| ['diff', '--quiet', '--ignore-submodules', initial_commit_hash], |
| check=False) |
| if r.returncode: |
| sys.exit(f''' |
| Failed to recreate the pre-import tree by importing the prior Wayland version |
| {current_version} and applying the Android fixups. |
| |
| This is likely due to changes having been made to the Wayland sources without |
| a corresponding patch file in patches/. |
| |
| To see the differences detected, run: |
| |
| git diff {initial_commit_hash} |
| '''.strip()) |
| |
| if args.version: |
| print(f'Importing {args.version}') |
| import_sources(args.version, preserve_files) |
| apply_and_reexport_patches( |
| args.version, |
| (patch_hashes if patch_hashes is not None else patch_files), |
| use_cherry_pick=(patch_hashes is not None)) |
| |
| if args.squash: |
| print('Squashing to one commit') |
| git(['reset', '--soft', initial_commit_hash]) |
| git([ |
| 'commit', '--allow-empty', '-m', f''' |
| Update to Wayland {args.version} |
| |
| Automatic import using "./import_official_snapshot.py {args.version}" |
| '''.strip() |
| ]) |
| |
| |
| if __name__ == '__main__': |
| main() |