1#! /usr/bin/env nix-shell
2#! nix-shell -i python -p python3.pkgs.joblib python3.pkgs.click python3.pkgs.click-log nix nix-prefetch-git 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
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
47memory: Memory = Memory("cache", verbose=0)
48
49logger = logging.getLogger(__name__)
50click_log.basic_config(logger)
51
52
53def get_gclient_data(rev: str) -> any:
54 output = subprocess.check_output(
55 ["gclient2nix", "generate",
56 f"https://github.com/electron/electron@{rev}",
57 "--root", "src/electron"]
58 )
59
60 return json.loads(output)
61
62
63def get_chromium_file(chromium_tag: str, filepath: str) -> str:
64 return base64.b64decode(
65 urlopen(
66 f"https://chromium.googlesource.com/chromium/src.git/+/{chromium_tag}/{filepath}?format=TEXT"
67 ).read()
68 ).decode("utf-8")
69
70
71def get_electron_file(electron_tag: str, filepath: str) -> str:
72 return (
73 urlopen(
74 f"https://raw.githubusercontent.com/electron/electron/{electron_tag}/{filepath}"
75 )
76 .read()
77 .decode("utf-8")
78 )
79
80
81@memory.cache
82def get_chromium_gn_source(chromium_tag: str) -> dict:
83 gn_pattern = r"'gn_version': 'git_revision:([0-9a-f]{40})'"
84 gn_commit = re.search(gn_pattern, get_chromium_file(chromium_tag, "DEPS")).group(1)
85 gn_prefetch: bytes = subprocess.check_output(
86 [
87 "nix-prefetch-git",
88 "--quiet",
89 "https://gn.googlesource.com/gn",
90 "--rev",
91 gn_commit,
92 ]
93 )
94 gn: dict = json.loads(gn_prefetch)
95 return {
96 "gn": {
97 "version": datetime.fromisoformat(gn["date"]).date().isoformat(),
98 "url": gn["url"],
99 "rev": gn["rev"],
100 "hash": gn["hash"],
101 }
102 }
103
104@memory.cache
105def get_electron_yarn_hash(electron_tag: str) -> str:
106 print(f"prefetch-yarn-deps", file=sys.stderr)
107 with tempfile.TemporaryDirectory() as tmp_dir:
108 with open(tmp_dir + "/yarn.lock", "w") as f:
109 f.write(get_electron_file(electron_tag, "yarn.lock"))
110 return (
111 subprocess.check_output(["prefetch-yarn-deps", tmp_dir + "/yarn.lock"])
112 .decode("utf-8")
113 .strip()
114 )
115
116@memory.cache
117def get_chromium_npm_hash(chromium_tag: str) -> str:
118 print(f"prefetch-npm-deps", file=sys.stderr)
119 with tempfile.TemporaryDirectory() as tmp_dir:
120 with open(tmp_dir + "/package-lock.json", "w") as f:
121 f.write(get_chromium_file(chromium_tag, "third_party/node/package-lock.json"))
122 return (
123 subprocess.check_output(
124 ["prefetch-npm-deps", tmp_dir + "/package-lock.json"]
125 )
126 .decode("utf-8")
127 .strip()
128 )
129
130
131def get_update(major_version: str, m: str, gclient_data: any) -> Tuple[str, dict]:
132
133 tasks = []
134 a = lambda: (("electron_yarn_hash", get_electron_yarn_hash(gclient_data["src/electron"]["args"]["tag"])))
135 tasks.append(delayed(a)())
136 a = lambda: (
137 (
138 "chromium_npm_hash",
139 get_chromium_npm_hash(gclient_data["src"]["args"]["tag"]),
140 )
141 )
142 tasks.append(delayed(a)())
143 random.shuffle(tasks)
144
145 task_results = {
146 n[0]: n[1]
147 for n in Parallel(n_jobs=3, require="sharedmem", return_as="generator")(tasks)
148 if n != None
149 }
150
151 return (
152 f"{major_version}",
153 {
154 "deps": gclient_data,
155 **{key: m[key] for key in ["version", "modules", "chrome", "node"]},
156 "chromium": {
157 "version": m["chrome"],
158 "deps": get_chromium_gn_source(gclient_data["src"]["args"]["tag"]),
159 },
160 **task_results,
161 },
162 )
163
164
165def non_eol_releases(releases: Iterable[int]) -> Iterable[int]:
166 """Returns a list of releases that have not reached end-of-life yet."""
167 return tuple(filter(lambda x: x in supported_version_range(), releases))
168
169
170def update_source(version: str, commit: bool) -> None:
171 """Update a given electron-source release
172
173 Args:
174 version: The major version number, e.g. '27'
175 commit: Whether the updater should commit the result
176 """
177 major_version = version
178
179 package_name = f"electron-source.electron_{major_version}"
180 print(f"Updating electron-source.electron_{major_version}")
181
182 old_info = load_info_json(SOURCE_INFO_JSON)
183 old_version = (
184 old_info[major_version]["version"]
185 if major_version in old_info
186 else None
187 )
188
189 m, rev = get_latest_version(major_version)
190 if old_version == m["version"]:
191 print(f"{package_name} is up-to-date")
192 return
193
194 gclient_data = get_gclient_data(rev)
195 new_info = get_update(major_version, m, gclient_data)
196 out = old_info | {new_info[0]: new_info[1]}
197
198 save_info_json(SOURCE_INFO_JSON, out)
199
200 new_version = new_info[1]["version"]
201 if commit:
202 commit_result(package_name, old_version, new_version, SOURCE_INFO_JSON)
203
204
205@click.group()
206def cli() -> None:
207 """A script for updating electron-source hashes"""
208 pass
209
210
211@cli.command("update", help="Update a single major release")
212@click.option("-v", "--version", required=True, type=str, help="The major version, e.g. '23'")
213@click.option("-c", "--commit", is_flag=True, default=False, help="Commit the result")
214def update(version: str, commit: bool) -> None:
215 update_source(version, commit)
216
217
218@cli.command("update-all", help="Update all releases at once")
219@click.option("-c", "--commit", is_flag=True, default=False, help="Commit the result")
220def update_all(commit: bool) -> None:
221 """Update all eletron-source releases at once
222
223 Args:
224 commit: Whether to commit the result
225 """
226 old_info = load_info_json(SOURCE_INFO_JSON)
227
228 filtered_releases = non_eol_releases(tuple(map(lambda x: int(x), old_info.keys())))
229
230 for major_version in filtered_releases:
231 update_source(str(major_version), commit)
232
233
234if __name__ == "__main__":
235 cli()