1#! /usr/bin/env nix-shell
2#! nix-shell -i python -p python3.pkgs.joblib python3.pkgs.click python3.pkgs.click-log nix nurl prefetch-yarn-deps prefetch-npm-deps gclient2nix
3"""
4electron updater
5
6A script for updating electron source hashes.
7
8It supports the following modes:
9
10| Mode | Description |
11|------------- | ----------------------------------------------- |
12| `update` | for updating a specific Electron release |
13| `update-all` | for updating all electron releases at once |
14
15The `update` commands requires a `--version` flag
16to specify the major release to be updated.
17The `update-all command updates all non-eol major releases.
18
19The `update` and `update-all` commands accept an optional `--commit`
20flag to automatically commit the changes for you.
21"""
22import base64
23import json
24import logging
25import os
26import random
27import re
28import subprocess
29import sys
30import tempfile
31import urllib.request
32import click
33import click_log
34
35from datetime import datetime, UTC
36from typing import Iterable, Tuple
37from urllib.request import urlopen
38from joblib import Parallel, delayed, Memory
39from update_util import *
40
41
42# Relative path to the electron-source info.json
43SOURCE_INFO_JSON = "info.json"
44
45os.chdir(os.path.dirname(__file__))
46
47# Absolute path of nixpkgs top-level directory
48NIXPKGS_PATH = subprocess.check_output(["git", "rev-parse", "--show-toplevel"]).decode("utf-8").strip()
49
50memory: Memory = Memory("cache", verbose=0)
51
52logger = logging.getLogger(__name__)
53click_log.basic_config(logger)
54
55
56def get_gclient_data(rev: str) -> any:
57 output = subprocess.check_output(
58 ["gclient2nix", "generate",
59 f"https://github.com/electron/electron@{rev}",
60 "--root", "src/electron"]
61 )
62
63 return json.loads(output)
64
65
66def get_chromium_file(chromium_tag: str, filepath: str) -> str:
67 return base64.b64decode(
68 urlopen(
69 f"https://chromium.googlesource.com/chromium/src.git/+/{chromium_tag}/{filepath}?format=TEXT"
70 ).read()
71 ).decode("utf-8")
72
73
74def get_electron_file(electron_tag: str, filepath: str) -> str:
75 return (
76 urlopen(
77 f"https://raw.githubusercontent.com/electron/electron/{electron_tag}/{filepath}"
78 )
79 .read()
80 .decode("utf-8")
81 )
82
83
84@memory.cache
85def get_gn_hash(gn_version, gn_commit):
86 print("gn.override", file=sys.stderr)
87 expr = f'(import {NIXPKGS_PATH} {{}}).gn.override {{ version = "{gn_version}"; rev = "{gn_commit}"; hash = ""; }}'
88 out = subprocess.check_output(["nurl", "--hash", "--expr", expr])
89 return out.decode("utf-8").strip()
90
91@memory.cache
92def get_chromium_gn_source(chromium_tag: str) -> dict:
93 gn_pattern = r"'gn_version': 'git_revision:([0-9a-f]{40})'"
94 gn_commit = re.search(gn_pattern, get_chromium_file(chromium_tag, "DEPS")).group(1)
95
96 gn_commit_info = json.loads(
97 urlopen(f"https://gn.googlesource.com/gn/+/{gn_commit}?format=json")
98 .read()
99 .decode("utf-8")
100 .split(")]}'\n")[1]
101 )
102
103 gn_commit_date = datetime.strptime(gn_commit_info["committer"]["time"], "%a %b %d %H:%M:%S %Y %z")
104 gn_date = gn_commit_date.astimezone(UTC).date().isoformat()
105 gn_version = f"0-unstable-{gn_date}"
106
107 return {
108 "gn": {
109 "version": gn_version,
110 "rev": gn_commit,
111 "hash": get_gn_hash(gn_version, gn_commit),
112 }
113 }
114
115@memory.cache
116def get_electron_yarn_hash(electron_tag: str) -> str:
117 print(f"prefetch-yarn-deps", file=sys.stderr)
118 with tempfile.TemporaryDirectory() as tmp_dir:
119 with open(tmp_dir + "/yarn.lock", "w") as f:
120 f.write(get_electron_file(electron_tag, "yarn.lock"))
121 return (
122 subprocess.check_output(["prefetch-yarn-deps", tmp_dir + "/yarn.lock"])
123 .decode("utf-8")
124 .strip()
125 )
126
127@memory.cache
128def get_chromium_npm_hash(chromium_tag: str) -> str:
129 print(f"prefetch-npm-deps", file=sys.stderr)
130 with tempfile.TemporaryDirectory() as tmp_dir:
131 with open(tmp_dir + "/package-lock.json", "w") as f:
132 f.write(get_chromium_file(chromium_tag, "third_party/node/package-lock.json"))
133 return (
134 subprocess.check_output(
135 ["prefetch-npm-deps", tmp_dir + "/package-lock.json"]
136 )
137 .decode("utf-8")
138 .strip()
139 )
140
141
142def get_update(major_version: str, m: str, gclient_data: any) -> Tuple[str, dict]:
143
144 tasks = []
145 a = lambda: (("electron_yarn_hash", get_electron_yarn_hash(gclient_data["src/electron"]["args"]["tag"])))
146 tasks.append(delayed(a)())
147 a = lambda: (
148 (
149 "chromium_npm_hash",
150 get_chromium_npm_hash(gclient_data["src"]["args"]["tag"]),
151 )
152 )
153 tasks.append(delayed(a)())
154 random.shuffle(tasks)
155
156 task_results = {
157 n[0]: n[1]
158 for n in Parallel(n_jobs=3, require="sharedmem", return_as="generator")(tasks)
159 if n != None
160 }
161
162 return (
163 f"{major_version}",
164 {
165 "deps": gclient_data,
166 **{key: m[key] for key in ["version", "modules", "chrome", "node"]},
167 "chromium": {
168 "version": m["chrome"],
169 "deps": get_chromium_gn_source(gclient_data["src"]["args"]["tag"]),
170 },
171 **task_results,
172 },
173 )
174
175
176def non_eol_releases(releases: Iterable[int]) -> Iterable[int]:
177 """Returns a list of releases that have not reached end-of-life yet."""
178 return tuple(filter(lambda x: x in supported_version_range(), releases))
179
180
181def update_source(version: str, commit: bool) -> None:
182 """Update a given electron-source release
183
184 Args:
185 version: The major version number, e.g. '27'
186 commit: Whether the updater should commit the result
187 """
188 major_version = version
189
190 package_name = f"electron-source.electron_{major_version}"
191 print(f"Updating electron-source.electron_{major_version}")
192
193 old_info = load_info_json(SOURCE_INFO_JSON)
194 old_version = (
195 old_info[major_version]["version"]
196 if major_version in old_info
197 else None
198 )
199
200 m, rev = get_latest_version(major_version)
201 if old_version == m["version"]:
202 print(f"{package_name} is up-to-date")
203 return
204
205 gclient_data = get_gclient_data(rev)
206 new_info = get_update(major_version, m, gclient_data)
207 out = old_info | {new_info[0]: new_info[1]}
208
209 save_info_json(SOURCE_INFO_JSON, out)
210
211 new_version = new_info[1]["version"]
212 if commit:
213 commit_result(package_name, old_version, new_version, SOURCE_INFO_JSON)
214
215
216@click.group()
217def cli() -> None:
218 """A script for updating electron-source hashes"""
219 pass
220
221
222@cli.command("update", help="Update a single major release")
223@click.option("-v", "--version", required=True, type=str, help="The major version, e.g. '23'")
224@click.option("-c", "--commit", is_flag=True, default=False, help="Commit the result")
225def update(version: str, commit: bool) -> None:
226 update_source(version, commit)
227
228
229@cli.command("update-all", help="Update all releases at once")
230@click.option("-c", "--commit", is_flag=True, default=False, help="Commit the result")
231def update_all(commit: bool) -> None:
232 """Update all eletron-source releases at once
233
234 Args:
235 commit: Whether to commit the result
236 """
237 old_info = load_info_json(SOURCE_INFO_JSON)
238
239 filtered_releases = non_eol_releases(tuple(map(lambda x: int(x), old_info.keys())))
240
241 for major_version in filtered_releases:
242 update_source(str(major_version), commit)
243
244
245if __name__ == "__main__":
246 cli()