1#!/usr/bin/python3 2# Copyright 2021 The Android Open Source Project 3# 4# Licensed under the Apache License, Version 2.0 (the "License"); 5# you may not use this file except in compliance with the License. 6# You may obtain a copy of the License at 7# 8# http://www.apache.org/licenses/LICENSE-2.0 9# 10# Unless required by applicable law or agreed to in writing, software 11# distributed under the License is distributed on an "AS IS" BASIS, 12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13# See the License for the specific language governing permissions and 14# limitations under the License. 15 16import argparse 17import datetime 18import os 19import os.path 20import re 21import subprocess 22import sys 23""" 24Helper script for importing a new snapshot of the official Wayland sources. 25 26Usage: ./import_official_snapshot.py 1.18.0 27""" 28 29 30def git(cmd, check=True): 31 return subprocess.run(['git'] + cmd, 32 capture_output=True, 33 check=check, 34 text=True) 35 36 37def git_get_hash(commit): 38 return git(['show-ref', '--head', '--hash', commit]).stdout.strip() 39 40 41def git_is_ref(ref): 42 return git( 43 ['show-ref', '--head', '--hash', '--verify', f'refs/heads/{ref}'], 44 check=False).returncode == 0 45 46 47def get_git_files(version): 48 return set( 49 git(['ls-tree', '-r', '--name-only', 50 f'{version}^{{tree}}']).stdout.split()) 51 52 53def assert_no_uncommitted_changes(): 54 r = git(['diff-files', '--quiet', '--ignore-submodules'], check=False) 55 if r.returncode: 56 sys.exit('Error: Your tree is dirty') 57 58 r = git( 59 ['diff-index', '--quiet', '--ignore-submodules', '--cached', 'HEAD'], 60 check=False) 61 if r.returncode: 62 sys.exit('Error: You have staged changes') 63 64 65def metadata_read_current_version(): 66 with open('METADATA', 'rt') as metadata_file: 67 metadata = metadata_file.read() 68 69 match = re.search(r'version: "([^"]*)"', metadata) 70 if not match: 71 sys.exit('Error: Unable to determine current version from METADATA') 72 return match.group(1) 73 74 75def metadata_read_git_url(): 76 with open('METADATA', 'rt') as metadata_file: 77 metadata = metadata_file.read() 78 79 match = re.search(r'url\s*{\s*type:\s*GIT\s*value:\s*"([^"]*)"\s*}', 80 metadata) 81 if not match: 82 sys.exit('Error: Unable to determine GIT url from METADATA') 83 return match.group(1) 84 85 86def setup_and_update_official_source_remote(official_source_git_url): 87 r = git(['remote', 'get-url', 'official-source'], check=False) 88 if r.returncode or r.stdout != official_source_git_url: 89 # Not configured as expected. 90 print( 91 f' Configuring official-source remote {official_source_git_url}') 92 git(['remote', 'remove', 'official-source'], check=False) 93 git(['remote', 'add', 'official-source', official_source_git_url]) 94 95 print(' Syncing official-source') 96 git(['remote', 'update', 'official-source']) 97 98 99def get_local_files(): 100 result = [] 101 for root, dirs, files in os.walk('.'): 102 # Don't include ./.git 103 if root == '.' and '.git' in dirs: 104 dirs.remove('.git') 105 for name in files: 106 result.append(os.path.join(root, name)[2:]) 107 return result 108 109 110def determine_files_to_preserve(current_version): 111 local_files = set(get_local_files()) 112 113 current_official_files = get_git_files(current_version) 114 115 android_added_files = local_files - current_official_files 116 117 return android_added_files 118 119 120def update_metadata_version_and_import_date(version): 121 now = datetime.datetime.now() 122 123 with open('METADATA', 'rt') as metadata_file: 124 metadata = metadata_file.read() 125 126 metadata = re.sub(r'version: "[^"]*"', f'version: "{version}"', metadata) 127 metadata = re.sub( 128 r'last_upgrade_date {[^}]*}', 129 (f'last_upgrade_date {{ year: {now.year} month: {now.month} ' 130 f'day: {now.day} }}'), metadata) 131 132 with open('METADATA', 'wt') as metadata_file: 133 metadata_file.write(metadata) 134 135 136def configure_wayland_version_header(version): 137 with open('./src/wayland-version.h.in', 'rt') as template_file: 138 content = template_file.read() 139 140 (major, minor, micro) = version.split('.') 141 142 content = re.sub(r'@WAYLAND_VERSION_MAJOR@', major, content) 143 content = re.sub(r'@WAYLAND_VERSION_MINOR@', minor, content) 144 content = re.sub(r'@WAYLAND_VERSION_MICRO@', micro, content) 145 content = re.sub(r'@WAYLAND_VERSION@', version, content) 146 147 with open('./src/wayland-version.h', 'wt') as version_header: 148 version_header.write(content) 149 150 # wayland-version.h is in .gitignore, so we explicitly have to force-add it. 151 git(['add', '-f', './src/wayland-version.h']) 152 153 154def import_sources(version, preserve_files, update_metdata=True): 155 start_hash = git_get_hash('HEAD') 156 157 # Use `git-read-tree` to start with a pure copy of the imported version 158 git(['read-tree', '-m', '-u', f'{version}^{{tree}}']) 159 160 git(['commit', '-m', f'To squash: Clean import of {version}']) 161 162 print(' Adding Android metadata') 163 164 # Restore the needed Android files 165 git(['restore', '--staged', '--worktree', '--source', start_hash] + 166 list(sorted(preserve_files))) 167 168 if update_metdata: 169 update_metadata_version_and_import_date(version) 170 configure_wayland_version_header(version) 171 172 git(['commit', '-a', '-m', f'To squash: Update versions {version}']) 173 174 175def apply_and_reexport_patches(version, patches, use_cherry_pick=False): 176 if not patches: 177 return 178 179 print(f' Applying {len(patches)} Android patches') 180 181 try: 182 if use_cherry_pick: 183 git(['cherry-pick'] + patches) 184 else: 185 git(['am'] + patches) 186 except subprocess.CalledProcessError as e: 187 if 'patch failed' not in e.stderr: 188 raise 189 # Print out the captured error mess 190 sys.stderr.write(f''' 191Failure applying patches to Wayland {version} via: 192 {e.cmd} 193 194Once the patches have been resolved, please re-export the patches with: 195 196 git rm patches/*.diff 197 git format-patch HEAD~{len(patches)}..HEAD --no-stat --no-signature \\ 198 --numbered --zero-commit --suffix=.diff --output-directory patches 199 200... and also add them to the final squashed commit. 201 '''.strip()) 202 203 sys.stdout.write(e.stdout) 204 sys.exit(e.stderr) 205 206 patch_hashes = list( 207 reversed( 208 git(['log', f'-{len(patches)}', 209 '--pretty=format:%H']).stdout.split())) 210 211 # Clean out the existing patches 212 git(['rm', 'patches/*.diff']) 213 214 # Re-export the patches, omitting information that might change 215 git([ 216 'format-patch', f'HEAD~{len(patches)}..HEAD', '--no-stat', 217 '--no-signature', '--numbered', '--zero-commit', '--suffix=.diff', 218 '--output-directory', 'patches' 219 ]) 220 221 # Add back all the exported patches 222 git(['add', 'patches/*.diff']) 223 224 # Create a commit for the exported patches if there are any differences. 225 r = git(['diff-files', '--quiet', '--ignore-submodules'], check=False) 226 if r.returncode: 227 git(['commit', '-a', '-m', f'To squash: Update patches for {version}']) 228 229 return patch_hashes 230 231 232def main(): 233 parser = argparse.ArgumentParser(description=( 234 "Helper script for importing a snapshot of the Wayland sources.")) 235 parser.add_argument('version', 236 nargs='?', 237 default=None, 238 help='The official version to import') 239 parser.add_argument( 240 '--no-validate-existing', 241 dest='validate_existing', 242 default=True, 243 action='store_false', 244 help='Whether to validate the current tree against upstream + patches') 245 parser.add_argument('--no-squash', 246 dest='squash', 247 default=True, 248 action='store_false', 249 help='Whether to squash the import to a single commit') 250 args = parser.parse_args() 251 252 print( 253 f'Preparing to importing Wayland core sources version {args.version}') 254 255 assert_no_uncommitted_changes() 256 257 official_source_git_url = metadata_read_git_url() 258 current_version = metadata_read_current_version() 259 260 setup_and_update_official_source_remote(official_source_git_url) 261 262 # Get the list of Android added files to preserve 263 preserve_files = determine_files_to_preserve(current_version) 264 265 # Filter the list to get all patches that we will need to apply 266 patch_files = sorted(path for path in preserve_files 267 if path.startswith('patches/')) 268 269 # Detect any add/add conflicts before we begin 270 new_files = get_git_files(args.version or current_version) 271 add_add_conflicts = preserve_files.intersection(new_files) 272 if add_add_conflicts: 273 sys.exit(f''' 274Error: The new version of Wayland adds files that are also added for Android: 275{add_add_conflicts} 276 '''.strip()) 277 278 import_branch_name = f'import_{args.version}' if args.version else None 279 280 if import_branch_name and git_is_ref(import_branch_name): 281 sys.exit(f''' 282Error: Branch name {import_branch_name} already exists. Please delete or rename. 283 '''.strip()) 284 285 initial_commit_hash = git_get_hash('HEAD') 286 287 # Begin a branch for the version import, if a new version is being imported 288 if import_branch_name: 289 git(['checkout', '-b', import_branch_name]) 290 git([ 291 'commit', '--allow-empty', '-m', 292 f'Update to Wayland {args.version}' 293 ]) 294 295 patch_hashes = None 296 297 if args.validate_existing: 298 print(f'Importing {current_version} to validate all current fixups') 299 import_sources(current_version, preserve_files, update_metdata=False) 300 patch_hashes = apply_and_reexport_patches(current_version, patch_files) 301 302 r = git( 303 ['diff', '--quiet', '--ignore-submodules', initial_commit_hash], 304 check=False) 305 if r.returncode: 306 sys.exit(f''' 307Failed to recreate the pre-import tree by importing the prior Wayland version 308{current_version} and applying the Android fixups. 309 310This is likely due to changes having been made to the Wayland sources without 311a corresponding patch file in patches/. 312 313To see the differences detected, run: 314 315git diff {initial_commit_hash} 316 '''.strip()) 317 318 if args.version: 319 print(f'Importing {args.version}') 320 import_sources(args.version, preserve_files) 321 apply_and_reexport_patches( 322 args.version, 323 (patch_hashes if patch_hashes is not None else patch_files), 324 use_cherry_pick=(patch_hashes is not None)) 325 326 if args.squash: 327 print('Squashing to one commit') 328 git(['reset', '--soft', initial_commit_hash]) 329 git([ 330 'commit', '--allow-empty', '-m', f''' 331Update to Wayland {args.version} 332 333Automatic import using "./import_official_snapshot.py {args.version}" 334 '''.strip() 335 ]) 336 337 338if __name__ == '__main__': 339 main() 340