xref: /aosp_15_r20/external/wayland/import_official_snapshot.py (revision 84e872a0dc482bffdb63672969dd03a827d67c73)
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