Skip to content

Commit fd6a588

Browse files
committed
[GR-21590][GR-71934] Update imports.
PullRequest: graalpython/4191
2 parents c6d4bbf + 61a8e1a commit fd6a588

565 files changed

Lines changed: 12874 additions & 46962 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/scripts/extract_matrix.py

Lines changed: 371 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,371 @@
1+
# Copyright (c) 2025, 2026, Oracle and/or its affiliates. All rights reserved.
2+
# DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
3+
#
4+
# The Universal Permissive License (UPL), Version 1.0
5+
#
6+
# Subject to the condition set forth below, permission is hereby granted to any
7+
# person obtaining a copy of this software, associated documentation and/or
8+
# data (collectively the "Software"), free of charge and under any and all
9+
# copyright rights in the Software, and any and all patent rights owned or
10+
# freely licensable by each licensor hereunder covering either (i) the
11+
# unmodified Software as contributed to or provided by such licensor, or (ii)
12+
# the Larger Works (as defined below), to deal in both
13+
#
14+
# (a) the Software, and
15+
#
16+
# (b) any piece of software and/or hardware listed in the lrgrwrks.txt file if
17+
# one is included with the Software each a "Larger Work" to which the Software
18+
# is contributed by such licensors),
19+
#
20+
# without restriction, including without limitation the rights to copy, create
21+
# derivative works of, display, perform, and distribute the Software and make,
22+
# use, sell, offer for sale, import, export, have made, and have sold the
23+
# Software and the Larger Work(s), and to sublicense the foregoing rights on
24+
# either these or other terms.
25+
#
26+
# This license is subject to the following condition:
27+
#
28+
# The above copyright notice and either this complete permission notice or at a
29+
# minimum a reference to the UPL must be included in all copies or substantial
30+
# portions of the Software.
31+
#
32+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
33+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
34+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
35+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
36+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
37+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
38+
# SOFTWARE.
39+
40+
#!/usr/bin/env python3
41+
import argparse
42+
import fnmatch
43+
import json
44+
import os
45+
import re
46+
import shlex
47+
import subprocess
48+
import sys
49+
50+
from dataclasses import dataclass
51+
from functools import cached_property, total_ordering
52+
from typing import Any
53+
54+
DEFAULT_ENV = {
55+
"CI": "true",
56+
"PYTHONIOENCODING": "utf-8",
57+
"GITHUB_CI": "true"
58+
}
59+
60+
# If any of these terms are in the job json, they do not run in public
61+
# infrastructure
62+
JOB_EXCLUSION_TERMS = (
63+
"enterprise",
64+
"corporate-compliance",
65+
66+
# Jobs failing in GitHub Actions:buffer overflow, out of memory
67+
"python-svm-unittest",
68+
"cpython-gate",
69+
70+
"darwin",
71+
)
72+
73+
DOWNLOADS_LINKS = {
74+
"GRADLE_JAVA_HOME": "https://download.oracle.com/java/{major_version}/latest/jdk-{major_version}_{os}-{arch_short}_bin{ext}"
75+
}
76+
77+
# Gitlab Runners OSS
78+
OSS = {
79+
"macos-latest": ["darwin", "aarch64"],
80+
"ubuntu-latest": ["linux", "amd64"],
81+
"ubuntu-24.04-arm": ["linux", "aarch64"],
82+
"windows-latest": ["windows", "amd64"]
83+
}
84+
85+
# Override unavailable Python versions for some OS/Arch combinations
86+
PYTHON_VERSIONS = {
87+
"ubuntu-24.04-arm": "3.12.8",
88+
}
89+
90+
91+
@dataclass
92+
class Artifact:
93+
name: str
94+
pattern: str
95+
96+
97+
@total_ordering
98+
class Job:
99+
def __init__(self, job: dict[str, Any]):
100+
self.job = job
101+
102+
@cached_property
103+
def runs_on(self) -> str:
104+
capabilities = self.job.get("capabilities", [])
105+
106+
for os, caps in OSS.items():
107+
if all(required in capabilities for required in caps): return os
108+
109+
return "ubuntu-latest"
110+
111+
@cached_property
112+
def name(self) -> str:
113+
return self.job["name"]
114+
115+
@cached_property
116+
def targets(self) -> list[str]:
117+
return self.job.get("targets", [])
118+
119+
@cached_property
120+
def env(self) -> dict[str, str]:
121+
environment = self.job.get("environment", {}) | DEFAULT_ENV
122+
if self.runs_on == "windows-latest":
123+
def _to_windows_env_format(s: str) -> str:
124+
# Replace ${VAR} and $VAR with %VAR%
125+
s = re.sub(r"\$\{([A-Za-z_][A-Za-z0-9_]*)\}", r"%\1%", s)
126+
s = re.sub(r"\$([A-Za-z_][A-Za-z0-9_]*)", r"%\1%", s)
127+
return s
128+
environment = {k: _to_windows_env_format(v) for k, v in environment.items()}
129+
return environment
130+
131+
@cached_property
132+
def mx_version(self) -> str | None:
133+
for k, v in self.job.get("packages", {}).items():
134+
if k == "mx":
135+
return v.strip("=<>~")
136+
137+
@cached_property
138+
def python_version(self) -> str | None:
139+
python_version = None
140+
for k, v in self.job.get("packages", {}).items():
141+
if k == "python3":
142+
python_version = v.strip("=<>~")
143+
for k, v in self.job.get("downloads", {}).items():
144+
if k == "PYTHON3_HOME":
145+
python_version = v.get("version", python_version)
146+
if "MX_PYTHON" in self.env:
147+
del self.env["MX_PYTHON"]
148+
if "MX_PYTHON_VERSION" in self.env:
149+
del self.env["MX_PYTHON_VERSION"]
150+
151+
if self.runs_on in PYTHON_VERSIONS:
152+
python_version = PYTHON_VERSIONS[self.runs_on]
153+
return python_version
154+
155+
@cached_property
156+
def system_packages(self) -> list[str]:
157+
# TODO: support more packages
158+
system_packages = []
159+
for k, _ in self.job.get("packages", {}).items():
160+
if k in ["mx", "python3"]:
161+
continue
162+
if k.startswith("pip:"):
163+
continue
164+
elif k.startswith("00:") or k.startswith("01:"):
165+
k = k[3:]
166+
system_packages.append(f"'{k}'" if self.runs_on != "windows-latest" else f"{k}")
167+
return system_packages
168+
169+
@cached_property
170+
def python_packages(self) -> list[str]:
171+
python_packages = []
172+
for k, v in self.job.get("packages", {}).items():
173+
if k.startswith("pip:"):
174+
python_packages.append(f"'{k[4:]}{v}'" if self.runs_on != "windows-latest" else f"{k[4:]}{v}")
175+
return python_packages
176+
177+
def get_download_steps(self, key: str, version: str) -> str:
178+
download_link = self.get_download_link(key, version)
179+
filename = download_link.split('/')[-1]
180+
181+
if self.runs_on == "windows-latest":
182+
return (f"""
183+
Invoke-WebRequest -Uri {download_link} -OutFile {filename}
184+
Expand-Archive -Path {filename} -DestinationPath .
185+
$dirname = (Get-ChildItem -Directory | Select-Object -First 1).Name
186+
Add-Content $env:GITHUB_ENV "{key}=$(Resolve-Path $dirname)"
187+
""")
188+
189+
return (f"wget -q {download_link} && "
190+
f"dirname=$(tar -tzf {filename} | head -1 | cut -f1 -d '/') && "
191+
f"tar -xzf {filename} && "
192+
f'echo {key}=$(realpath "$dirname") >> $GITHUB_ENV')
193+
194+
195+
def get_download_link(self, key: str, version: str) -> str:
196+
os, arch = OSS[self.runs_on]
197+
major_version = version.split(".")[0]
198+
extension = ".tar.gz" if not os == "windows" else ".zip"
199+
os = os if os != "darwin" else "macos"
200+
arch_short = {"amd64": "x64", "aarch64": "aarch64"}[arch]
201+
202+
vars = {
203+
"major_version": major_version,
204+
"os":os,
205+
"arch": arch,
206+
"arch_short": arch_short,
207+
"ext": extension,
208+
}
209+
210+
return DOWNLOADS_LINKS[key].format(**vars)
211+
212+
@cached_property
213+
def downloads(self) -> list[str]:
214+
downloads = []
215+
for k, download_info in self.job.get("downloads", {}).items():
216+
if k in DOWNLOADS_LINKS and download_info["version"]:
217+
downloads.append(self.get_download_steps(k, download_info["version"]))
218+
219+
return downloads
220+
221+
@staticmethod
222+
def common_glob(strings: list[str]) -> str:
223+
assert strings
224+
if len(strings) == 1:
225+
return strings[0]
226+
prefix = strings[0]
227+
for s in strings[1:]:
228+
i = 0
229+
while i < len(prefix) and i < len(s) and prefix[i] == s[i]:
230+
i += 1
231+
prefix = prefix[:i]
232+
if not prefix:
233+
break
234+
suffix = strings[0][len(prefix):]
235+
for s in strings[1:]:
236+
i = 1
237+
while i <= len(suffix) and i <= len(s) and suffix[-i] == s[-i]:
238+
i += 1
239+
if i == 1:
240+
suffix = ""
241+
break
242+
suffix = suffix[-(i-1):]
243+
return f"{prefix}*{suffix}"
244+
245+
@cached_property
246+
def upload_artifact(self) -> Artifact | None:
247+
if artifacts := self.job.get("publishArtifacts", []):
248+
assert len(artifacts) == 1
249+
dir = artifacts[0].get("dir", ".")
250+
patterns = artifacts[0].get("patterns", ["*"])
251+
return Artifact(
252+
artifacts[0]["name"],
253+
" ".join([os.path.normpath(os.path.join(dir, p)) for p in patterns])
254+
)
255+
return None
256+
257+
@cached_property
258+
def download_artifact(self) -> Artifact | None:
259+
if artifacts := self.job.get("requireArtifacts", []):
260+
pattern = self.common_glob([a["name"] for a in artifacts])
261+
return Artifact(pattern, os.path.normpath(artifacts[0].get("dir", ".")))
262+
return None
263+
264+
265+
@staticmethod
266+
def flatten_command(args: list[str | list[str]]) -> list[str]:
267+
flattened_args = []
268+
for s in args:
269+
if isinstance(s, list):
270+
flattened_args.append(f"$( {shlex.join(s)} )")
271+
else:
272+
flattened_args.append(s)
273+
return flattened_args
274+
275+
@cached_property
276+
def setup(self) -> str:
277+
cmds = [self.flatten_command(step) for step in self.job.get("setup", [])]
278+
return "\n".join(shlex.join(s) for s in cmds)
279+
280+
@cached_property
281+
def run(self) -> str:
282+
cmds = [self.flatten_command(step) for step in self.job.get("run", [])]
283+
return "\n".join(shlex.join(s) for s in cmds)
284+
285+
@cached_property
286+
def logs(self) -> str:
287+
return "\n".join(os.path.normpath(p) for p in self.job.get("logs", []))
288+
289+
def to_dict(self):
290+
"""
291+
This is the interchange with the YAML file defining the Github jobs, so here
292+
is where we must match the strings and expectations of the Github workflow.
293+
"""
294+
return {
295+
"name": self.name,
296+
"mx_version": self.mx_version,
297+
"os": self.runs_on,
298+
"python_version": self.python_version,
299+
"setup_steps": self.setup,
300+
"run_steps": self.run,
301+
"python_packages": " ".join(self.python_packages),
302+
"system_packages": " ".join(self.system_packages),
303+
"provide_artifact": [self.upload_artifact.name, self.upload_artifact.pattern] if self.upload_artifact else None,
304+
"require_artifact": [self.download_artifact.name, self.download_artifact.pattern] if self.download_artifact else None,
305+
"logs": self.logs.replace("../", "${{ env.PARENT_DIRECTORY }}/"),
306+
"env": self.env,
307+
"downloads_steps": " ".join(self.downloads),
308+
}
309+
310+
def __str__(self):
311+
return str(self.to_dict())
312+
313+
def __eq__(self, other):
314+
if isinstance(other, Job):
315+
return self.to_dict() == other.to_dict()
316+
return NotImplemented
317+
318+
def __gt__(self, other):
319+
if isinstance(other, Job):
320+
if self.job.get("runAfter") == other.name:
321+
return True
322+
if self.download_artifact and not other.download_artifact:
323+
return True
324+
if self.download_artifact and other.upload_artifact:
325+
if fnmatch.fnmatch(other.upload_artifact.name, self.download_artifact.name):
326+
return True
327+
if not self.upload_artifact:
328+
return True
329+
return False
330+
return NotImplemented
331+
332+
333+
def get_tagged_jobs(buildspec, target, filter=None):
334+
jobs = [Job({"name": target}).to_dict()]
335+
for job in sorted([Job(build) for build in buildspec.get("builds", [])]):
336+
if not any(t for t in job.targets if t in [target]):
337+
if "weekly" in job.targets and target == "tier1": pass
338+
else:
339+
continue
340+
if filter and not re.match(filter, job.name):
341+
continue
342+
if [x for x in JOB_EXCLUSION_TERMS if x in str(job)]:
343+
continue
344+
jobs.append(job.to_dict())
345+
return jobs
346+
347+
348+
def main(jsonnet_bin, ci_jsonnet, target, filter=None, indent=False):
349+
350+
result = subprocess.check_output([jsonnet_bin, ci_jsonnet], text=True)
351+
buildspec = json.loads(result)
352+
tagged_jobs = get_tagged_jobs(buildspec, target, filter=filter)
353+
matrix = tagged_jobs
354+
print(json.dumps(matrix, indent=2 if indent else None))
355+
356+
357+
if __name__ == "__main__":
358+
parser = argparse.ArgumentParser(description="Generate GitHub CI matrix from Jsonnet buildspec.")
359+
parser.add_argument("jsonnet_bin", help="Path to jsonnet binary")
360+
parser.add_argument("ci_jsonnet", help="Path to ci.jsonnet spec")
361+
parser.add_argument("target", help="Target name (e.g., tier1)")
362+
parser.add_argument("filter", nargs="?", default=None, help="Regex filter for job names (optional)")
363+
parser.add_argument('--indent', action='store_true', help='Indent output JSON')
364+
args = parser.parse_args()
365+
main(
366+
jsonnet_bin=args.jsonnet_bin,
367+
ci_jsonnet=args.ci_jsonnet,
368+
target=args.target,
369+
filter=args.filter,
370+
indent=args.indent or sys.stdout.isatty(),
371+
)

0 commit comments

Comments
 (0)