1import subprocess
2import re
3import argparse
4import os
5import shutil
6import sys
7
8# v10.0.0 -> fail if release/v10.0 is not there
9# v10.0.1 -> update release/v10.0
10# v10.1.0 -> create release/v10.1 from release/v10.0 and update it
11# v10.1.1 -> update release/v10.1
12
13LOG = "[release_branch_updater.py]"
14
15def main():
16
17    arg_parser = argparse.ArgumentParser()
18    arg_parser.add_argument("--port-clone-tmpdir", default="port_tmpdir")
19    arg_parser.add_argument("--port-urls-path", default=os.path.join(os.path.dirname(__file__), "release_branch_updater_port_urls.txt"))
20    arg_parser.add_argument("--lvgl-path", default=os.path.join(os.path.dirname(__file__), ".."))
21    arg_parser.add_argument("--dry-run", action="store_true")
22    arg_parser.add_argument("--oldest-major", type=int)
23    args = arg_parser.parse_args()
24
25    port_clone_tmpdir = args.port_clone_tmpdir
26    port_urls_path = args.port_urls_path
27    lvgl_path = args.lvgl_path
28    dry_run = args.dry_run
29    oldest_major = args.oldest_major
30
31    lvgl_release_branches = get_release_branches(lvgl_path)
32    print(LOG, "LVGL release branches:", ", ".join(fmt_release(br) for br in lvgl_release_branches) or "(none)")
33    if oldest_major is not None:
34        lvgl_release_branches = [br for br in lvgl_release_branches if br[0] >= oldest_major]
35        print(LOG, 'LVGL release branches after "oldest-major" filter:',
36              ", ".join(fmt_release(br) for br in lvgl_release_branches) or "(none)")
37
38    with open(port_urls_path) as f:
39        urls = f.read()
40    urls = [url for url in map(str.strip, urls.splitlines()) if url]
41
42    # ensure this script creates the directory i.e. it doesn't belong to the user since it will rm -rf at the end
43    assert not os.path.exists(port_clone_tmpdir), "the port clone tmpdir should not exist yet"
44
45    for url in urls:
46        print(LOG, "working with port:", url)
47
48        subprocess.check_call(("git", "clone", url, port_clone_tmpdir))
49
50        port_release_branches = get_release_branches(port_clone_tmpdir)
51        print(LOG, "port release branches:", ", ".join(fmt_release(br) for br in port_release_branches) or "(none)")
52
53        # we want to
54        # 1. create (if necessary) the port's release branch
55        # 2. update the LVGL submodule to match the LVGL's release branch version
56        # 3. update the lv_conf.h based on the lv_conf.defaults
57
58        # from oldest to newest release...
59        for lvgl_branch in lvgl_release_branches:
60            print(LOG, f"attempting to update release branch {fmt_release(lvgl_branch)} ...")
61
62            port_does_not_have_the_branch = False
63            port_submodule_was_updated = False
64            port_lv_conf_h_was_updated = False
65
66            # if the branch does not exist in the port, create it from
67            # the closest minor of the same major.
68            if lvgl_branch in port_release_branches:
69                print(LOG, "... this port has a matching release branch.")
70                subprocess.check_call(("git", "-C", port_clone_tmpdir, "branch", "--track",
71                                       fmt_release(lvgl_branch),
72                                       f"origin/{fmt_release(lvgl_branch)}"))
73            else:
74                print(LOG, "... this port does not have this release branch minor ...")
75                port_does_not_have_the_branch = True
76
77                # get the port branch with this major and the next smallest minor
78                create_from = next((
79                    br
80                    for br in reversed(port_release_branches) # reverse it to get the newest (largest) minor
81                    if br[0] == lvgl_branch[0]     # same major
82                       and br[1] < lvgl_branch[1]  # smaller minor because exact minor does not exist
83                ), None)
84                if create_from is None:
85                    # there are no branches in the port that are this major
86                    # version. One must be created manually.
87                    print(LOG, "... this port has no major from which to create the minor. one must be created manually. continuing to next.")
88                    continue
89
90                print(LOG, f"... creating the new branch {fmt_release(lvgl_branch)} "
91                                             f"from {fmt_release(create_from)}")
92                subprocess.check_call(("git", "-C", port_clone_tmpdir, "branch",
93                                       fmt_release(lvgl_branch),   # new branch name
94                                       fmt_release(create_from)))  # start point
95
96                port_release_branches.append(lvgl_branch)
97                port_release_branches.sort()
98
99            # checkout the same release in both LVGL and the port
100            subprocess.check_call(("git", "-C", lvgl_path, "checkout", f"origin/{fmt_release(lvgl_branch)}"))
101            subprocess.check_call(("git", "-C", port_clone_tmpdir, "checkout", fmt_release(lvgl_branch)))
102
103            # update the submodule in the port if it exists
104            out = subprocess.check_output(("git", "-C", port_clone_tmpdir, "config", "--file",
105                                           ".gitmodules", "--get-regexp", "path"))
106            port_lvgl_submodule_path = next((
107                line.partition("lvgl.path ")[2]
108                for line
109                in out.decode().strip().splitlines()
110                if "lvgl.path " in line
111            ), None)
112            if port_lvgl_submodule_path is None:
113                print(LOG, "this port has no LVGL submodule")
114            else:
115                print(LOG, "lvgl submodule found in port at:", port_lvgl_submodule_path)
116
117                # get the SHA of LVGL in this release of LVGL
118                out = subprocess.check_output(("git", "-C", lvgl_path, "rev-parse", "--verify", "--quiet", "HEAD"))
119                lvgl_sha = out.decode().strip()
120                print(LOG, "the SHA of LVGL in this release should be:", lvgl_sha)
121
122                # get the SHA of LVGL this port wants to use in this release
123                out = subprocess.check_output(("git", "-C", port_clone_tmpdir, "rev-parse",
124                                               "--verify", "--quiet", f"HEAD:{port_lvgl_submodule_path}"))
125                port_lvgl_submodule_sha = out.decode().strip()
126                print(LOG, "the SHA of LVGL in the submodule of this port is:", port_lvgl_submodule_sha)
127
128                if lvgl_sha == port_lvgl_submodule_sha:
129                    print(LOG, "the submodule's version of LVGL is already up to date")
130                else:
131                    print(LOG, "the submodule's version of LVGL is NOT up to date")
132                    port_submodule_was_updated = True
133
134                    # update the version of the submodule in the index. no need to `git submodule update --init` it.
135                    # also no need to `git add .` afterwards because it stages the change.
136                    # 160000 is a git file mode which means submodule.
137                    subprocess.check_call(("git", "-C", port_clone_tmpdir, "update-index", "--cacheinfo",
138                                           f"160000,{lvgl_sha},{port_lvgl_submodule_path}"))
139
140            # update the lv_conf.h if there's an lv_conf.defaults
141            out = subprocess.check_output(("find", ".", "-name", "lv_conf.defaults", "-print", "-quit"), cwd=port_clone_tmpdir)
142            port_lv_conf_defaults = next(iter(out.decode().strip().splitlines()), None)
143            if port_lv_conf_defaults is None:
144                print(LOG, "this port has no lv_conf.defaults")
145            else:
146                out = subprocess.check_output(("find", ".", "-name", "lv_conf.h", "-print", "-quit"), cwd=port_clone_tmpdir)
147                port_lv_conf_h = next(iter(out.decode().strip().splitlines()), None)
148                if port_lv_conf_h is None:
149                    print(LOG, "this port has an lv_conf.defaults but no lv_conf.h")
150                else:
151                    subprocess.check_call((sys.executable, os.path.join(lvgl_path, "scripts/generate_lv_conf.py"),
152                                           "--defaults", os.path.abspath(os.path.join(port_clone_tmpdir, port_lv_conf_defaults)),
153                                           "--config", os.path.abspath(os.path.join(port_clone_tmpdir, port_lv_conf_h)), ))
154
155                    # check if lv_conf.h actually changed. it will not detect the submodule change as a false positive.
156                    out = subprocess.check_output(("git", "-C", port_clone_tmpdir, "diff"))
157                    diff = out.decode().strip()
158                    if not diff:
159                        print(LOG, "this port's lv_conf.h did NOT change")
160                    else:
161                        print(LOG, "this port's lv_conf.h changed")
162                        port_lv_conf_h_was_updated = True
163                        subprocess.check_call(("git", "-C", port_clone_tmpdir, "add", port_lv_conf_h))
164                        out = subprocess.check_output(("git", "-C", port_clone_tmpdir, "diff"))
165                        diff = out.decode().strip()
166                        assert not diff
167
168            if port_does_not_have_the_branch or port_submodule_was_updated or port_lv_conf_h_was_updated:
169                print(LOG, "changes were made. ready to push.")
170                # keep it brief for commit message 50 character limit suggestion.
171                # max length will be 50 characters in this case: "CI release edit: new branch. submodule. lv_conf.h."
172                commit_msg = ("CI release edit:"
173                              + (" new branch." if port_does_not_have_the_branch else "")
174                              + (" submodule." if port_submodule_was_updated else "")
175                              + (" lv_conf.h." if port_lv_conf_h_was_updated else "")
176                             )
177                print(LOG, f"commit message: '{commit_msg}'")
178                subprocess.check_call(("git", "-C", port_clone_tmpdir, "commit", "-m", commit_msg))
179                if dry_run:
180                    print(LOG, "this is a dry run so nothing will be pushed")
181                else:
182                    subprocess.check_call(("git", "-C", port_clone_tmpdir, "push",
183                                           *(("-u", "origin") if port_does_not_have_the_branch else ()),
184                                           fmt_release(lvgl_branch),
185                                          ))
186                    print(LOG, "the changes were pushed.")
187            else:
188                print(LOG, "nothing to push for this release. it is up to date.")
189
190        shutil.rmtree(port_clone_tmpdir)
191
192        print(LOG, "port update complete:", url)
193
194def get_release_branches(working_dir):
195
196    out = subprocess.check_output(("git", "-C", working_dir, "branch", "--quiet", "--format", "%(refname)", "--all"))
197    branches = out.decode().strip().splitlines()
198
199    release_versions = []
200    for branch_name in branches:
201        release_branch = re.fullmatch(r"refs/remotes/origin/release/v([0-9]+)\.([0-9]+)", branch_name)
202        if release_branch is None:
203            continue
204        release_versions.append((int(release_branch[1]), int(release_branch[2])))
205
206    release_versions.sort()
207
208    return release_versions
209
210def fmt_release(release_tuple):
211    return f"release/v{release_tuple[0]}.{release_tuple[1]}"
212
213if __name__ == "__main__":
214    main()
215