Skip to content

Commit 800f4e5

Browse files
committed
ci: script to sync and rework ArduinoCore-API submodule
Signed-off-by: Aymane Bahssain <aymane.bahssain@st.com>
1 parent 960754c commit 800f4e5

File tree

1 file changed

+399
-0
lines changed

1 file changed

+399
-0
lines changed

CI/update/stm32api.py

Lines changed: 399 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,399 @@
1+
import argparse
2+
import shutil
3+
import subprocess
4+
import sys
5+
from pathlib import Path
6+
7+
script_path = Path(__file__).parent.resolve()
8+
sys.path.append(str(script_path.parent))
9+
from utils import execute_cmd
10+
11+
# Core STM32 repo root (Arduino_Core_STM32)
12+
core_path = script_path.parent.parent.resolve()
13+
14+
# ArduinoCore-API submodule path
15+
api_submodule_path = core_path / "cores" / "arduino" / "api"
16+
17+
# Git remotes / branches
18+
arduino_api_remote_name = "arduinocoreapi"
19+
arduino_api_remote_url = "https://github.com/arduino/ArduinoCore-API.git"
20+
arduino_api_default_branch = "master" # upstream default branch
21+
api_local_branch = "main" # local branch used in the submodule
22+
23+
# -----------------------------------------------------------------------------
24+
# Helpers
25+
# -----------------------------------------------------------------------------
26+
27+
28+
def getOpenApiPatchesPrNumbers():
29+
30+
print("== Fetch open PRs targeting 'api-patches' ==")
31+
32+
label_name = "hardware neutral patches"
33+
try:
34+
# gh pr list --base api-patches --state open --json number --jq '.[].number'
35+
output = subprocess.check_output(
36+
[
37+
"gh",
38+
"pr",
39+
"list",
40+
"--repo",
41+
"stm32duino/ArduinoCore-API",
42+
"--base",
43+
"api-patches",
44+
"--state",
45+
"open",
46+
"--label",
47+
label_name,
48+
"--json",
49+
"number",
50+
"--jq",
51+
".[].number",
52+
],
53+
text=True,
54+
)
55+
except subprocess.CalledProcessError as e:
56+
print("ERROR: failed to fetch PR list via gh CLI")
57+
print(e)
58+
return []
59+
60+
pr_numbers = []
61+
for line in output.splitlines():
62+
line = line.strip()
63+
if not line:
64+
continue
65+
try:
66+
pr_numbers.append(int(line))
67+
except ValueError:
68+
print(f"WARNING: unexpected PR number value: {line}")
69+
pr_numbers.sort()
70+
print(f" Found open PRs on 'api-patches': {pr_numbers}")
71+
return pr_numbers
72+
73+
74+
def run_api(cmd, capture_output=True):
75+
76+
if capture_output:
77+
return subprocess.check_output(
78+
cmd, cwd=str(api_submodule_path), text=True
79+
).strip()
80+
else:
81+
subprocess.check_call(cmd, cwd=str(api_submodule_path))
82+
83+
84+
def checkApiSubmodule():
85+
86+
if not api_submodule_path.exists():
87+
print(f"Could not find ArduinoCore-API submodule: {api_submodule_path}!")
88+
print("Please run: git submodule update --init")
89+
sys.exit(1)
90+
91+
92+
def ensureArduinoRemote():
93+
94+
remotes = run_api(["git", "remote"]).splitlines()
95+
if arduino_api_remote_name not in remotes:
96+
print(f"Add remote {arduino_api_remote_name} -> {arduino_api_remote_url}")
97+
run_api(
98+
["git", "remote", "add", arduino_api_remote_name, arduino_api_remote_url],
99+
capture_output=False,
100+
)
101+
else:
102+
url = run_api(["git", "remote", "get-url", arduino_api_remote_name])
103+
if url != arduino_api_remote_url:
104+
print(
105+
f"Update URL of {arduino_api_remote_name}: {url} -> {arduino_api_remote_url}"
106+
)
107+
run_api(
108+
[
109+
"git",
110+
"remote",
111+
"set-url",
112+
arduino_api_remote_name,
113+
arduino_api_remote_url,
114+
],
115+
capture_output=False,
116+
)
117+
118+
119+
def ensureApiClean():
120+
status = run_api(["git", "status", "--porcelain"])
121+
if status:
122+
print("Working tree NOT clean in submodule cores/arduino/api, abort.")
123+
print(status)
124+
sys.exit(1)
125+
126+
127+
def abortAnyAm():
128+
129+
try:
130+
run_api(["git", "am", "--abort"], capture_output=False)
131+
print(" (git am --abort executed to clear previous state)")
132+
except subprocess.CalledProcessError:
133+
# No rebase-apply directory, nothing to abort
134+
pass
135+
136+
137+
def getLatestArduinoTag():
138+
139+
# Ensure we have up-to-date tags
140+
run_api(["git", "fetch", arduino_api_remote_name, "--tags"], capture_output=False)
141+
142+
# List tags sorted by creation date (newest first) and pick the first one
143+
tags = run_api(["git", "tag", "--sort=-creatordate"]).splitlines()
144+
if not tags:
145+
print("WARNING: no tags found on arduinocoreapi, falling back to master")
146+
return None
147+
148+
latest_tag = tags[0].strip()
149+
print(f"Latest ArduinoCore-API tag: {latest_tag}")
150+
return latest_tag
151+
152+
153+
def resyncApiToArduinoLatestTag():
154+
155+
print("== Fetch arduinocoreapi (including tags) in cores/arduino/api ==")
156+
run_api(["git", "fetch", arduino_api_remote_name, "--tags"], capture_output=False)
157+
158+
print(f"== Checkout {api_local_branch} ==")
159+
run_api(["git", "checkout", api_local_branch], capture_output=False)
160+
161+
latest_tag = getLatestArduinoTag()
162+
if latest_tag is None:
163+
# fallback: use master HEAD if no tag exists
164+
target = f"{arduino_api_remote_name}/{arduino_api_default_branch}"
165+
else:
166+
# Reset directly to that tag (not to a describe string)
167+
target = latest_tag
168+
169+
print(f"== Reset {api_local_branch} to {target} (local only) ==")
170+
run_api(["git", "reset", "--hard", target], capture_output=False)
171+
172+
head = run_api(["git", "rev-parse", "HEAD"])
173+
log = run_api(["git", "log", "-1", "--oneline"])
174+
print(f" HEAD after reset : {head}")
175+
print(f" Last commit : {log}")
176+
177+
178+
def applyArduinoApiPr(pr_number):
179+
180+
print(f"== Download and apply stm32duino PR #{pr_number} using gh CLI == ")
181+
182+
# gh pr diff --repo stm32duino/ArduinoCore-API <PR> --patch
183+
try:
184+
patch_content = subprocess.check_output(
185+
[
186+
"gh",
187+
"pr",
188+
"diff",
189+
"--repo",
190+
"stm32duino/ArduinoCore-API",
191+
str(pr_number),
192+
"--patch",
193+
],
194+
text=True,
195+
)
196+
except subprocess.CalledProcessError as e:
197+
print(f"ERROR: failed to fetch PR #{pr_number} patch via gh CLI")
198+
print(e)
199+
sys.exit(1)
200+
201+
# Check that the patch applies cleanly using stdin
202+
print(" Check patch with git apply --check ...")
203+
check = subprocess.Popen(
204+
["git", "apply", "--check", "-"],
205+
cwd=str(api_submodule_path),
206+
stdin=subprocess.PIPE,
207+
text=True,
208+
)
209+
check.communicate(input=patch_content)
210+
if check.returncode != 0:
211+
print(f"ERROR: patch for PR #{pr_number} does not apply cleanly.")
212+
sys.exit(1)
213+
214+
# Ensure no previous git am is in progress
215+
abortAnyAm()
216+
217+
print(" Apply patch with git am ...")
218+
am = subprocess.Popen(
219+
["git", "am", "--keep-non-patch", "--signoff"],
220+
cwd=str(api_submodule_path),
221+
stdin=subprocess.PIPE,
222+
text=True,
223+
)
224+
am.communicate(input=patch_content)
225+
if am.returncode != 0:
226+
print(f"ERROR: git am failed for PR #{pr_number}.")
227+
sys.exit(1)
228+
229+
230+
def applyPatch(repo_path):
231+
232+
# First check if some patch need to be applied
233+
api_patch_path = script_path / "patch" / "api"
234+
patch_list = []
235+
for file in sorted(api_patch_path.iterdir()):
236+
if file.name.endswith(".patch"):
237+
patch_list.append(api_patch_path / file)
238+
239+
if len(patch_list):
240+
patch_failed = []
241+
print(f"Apply {len(patch_list)} patch{'' if len(patch_list) == 1 else 'es'}")
242+
for patch in patch_list:
243+
try:
244+
# Test the patch before apply it
245+
status = execute_cmd(
246+
["git", "-C", repo_path, "apply", "--check", str(patch)],
247+
subprocess.STDOUT,
248+
)
249+
if status:
250+
# patch can't be applied cleanly
251+
patch_failed.append([patch, status])
252+
continue
253+
# Apply the patch
254+
status = execute_cmd(
255+
[
256+
"git",
257+
"-C",
258+
repo_path,
259+
"am",
260+
"--keep-non-patch",
261+
"--quiet",
262+
"--signoff",
263+
str(patch),
264+
],
265+
None,
266+
)
267+
except subprocess.CalledProcessError as e:
268+
patch_failed.append([patch, e.cmd, e.output.decode("utf-8")])
269+
270+
if len(patch_failed):
271+
for fp in patch_failed:
272+
e_out = "" if len(fp) == 2 else f"\n--> {fp[2]}"
273+
print(f"Failed to apply {fp[0]}:\n{fp[1]}{e_out}")
274+
275+
276+
def reworkTree():
277+
278+
print("== Rework ArduinoCore-API tree for STM32 ==")
279+
280+
keep_files_exact = {"LICENSE"}
281+
282+
for entry in api_submodule_path.iterdir():
283+
name = entry.name
284+
285+
# Never touch .git
286+
if name == ".git":
287+
continue
288+
289+
# Keep .github directory
290+
if name == ".github" and entry.is_dir():
291+
continue
292+
293+
# Keep api directory (we transform inside it)
294+
if name == "api" and entry.is_dir():
295+
continue
296+
297+
# Keep LICENSE
298+
if name in keep_files_exact and entry.is_file():
299+
continue
300+
301+
# Keep README* files
302+
if entry.is_file() and name.startswith("README"):
303+
continue
304+
305+
# Everything else is removed
306+
if entry.is_dir():
307+
print(f" Remove directory: {entry}")
308+
shutil.rmtree(entry)
309+
else:
310+
print(f" Remove file : {entry}")
311+
entry.unlink()
312+
313+
# Flatten api/* into root (cores/arduino/api/)
314+
api_dir = api_submodule_path / "api"
315+
316+
if not api_dir.exists():
317+
print(" No 'api' directory found to flatten, nothing to do.")
318+
return
319+
320+
print(f" Flattening {api_dir} into {api_submodule_path}")
321+
322+
for entry in api_dir.iterdir():
323+
dest = api_submodule_path / entry.name
324+
if dest.exists():
325+
print(f" WARNING: destination already exists, skipping: {dest}")
326+
continue
327+
print(f" Move {entry} -> {dest}")
328+
entry.rename(dest)
329+
330+
# Remove now-empty api/ directory
331+
try:
332+
api_dir.rmdir()
333+
print(f" Removed empty directory: {api_dir}")
334+
except OSError:
335+
print(f" WARNING: {api_dir} not empty, please check manually.")
336+
337+
338+
def commitReworkedTree():
339+
340+
print("== Commit reworked ArduinoCore-API tree ==")
341+
342+
status = run_api(["git", "status", "--porcelain"])
343+
if not status:
344+
print(" No changes to commit.")
345+
return
346+
347+
run_api(["git", "add", "--all"], capture_output=False)
348+
349+
commit_msg = "refactor: keep only api/ folder for STM32duino usage"
350+
run_api(
351+
["git", "commit", "--all", "--signoff", f"--message={commit_msg}"],
352+
capture_output=False,
353+
)
354+
355+
print(" Commit created:")
356+
log = run_api(["git", "log", "-1", "--oneline"])
357+
print(f" {log}")
358+
359+
360+
# -----------------------------------------------------------------------------
361+
# main
362+
# -----------------------------------------------------------------------------
363+
364+
365+
def main():
366+
parser = argparse.ArgumentParser(
367+
description="Sync and patch ArduinoCore-API submodule for STM32 core"
368+
)
369+
_ = parser.parse_args()
370+
371+
print(f"Core repo path : {core_path}")
372+
print(f"API submodule : {api_submodule_path}")
373+
374+
checkApiSubmodule()
375+
ensureArduinoRemote()
376+
ensureApiClean()
377+
resyncApiToArduinoLatestTag()
378+
379+
pr_numbers = getOpenApiPatchesPrNumbers()
380+
381+
# Apply stm32duino API-upstream PRs via GH CLI
382+
for pr in pr_numbers:
383+
applyArduinoApiPr(pr)
384+
385+
applyPatch(api_submodule_path)
386+
387+
# Rework the tree layout for STM32 usage
388+
reworkTree()
389+
390+
# Commit the reworked tree
391+
commitReworkedTree()
392+
393+
print("== Log after applying PRs and reworking tree == ")
394+
log = run_api(["git", "log", "-5", "--oneline"])
395+
print(log)
396+
397+
398+
if __name__ == "__main__":
399+
main()

0 commit comments

Comments
 (0)