Skip to content

Commit 91a28bf

Browse files
committed
Improved upload script, added SBOM support
1 parent 0f27643 commit 91a28bf

File tree

4 files changed

+165
-37
lines changed

4 files changed

+165
-37
lines changed

sbom.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -694,8 +694,8 @@ def create_sbom_for_windows_artifact(
694694
sbom_cpython_package_spdx_id = spdx_id("SPDXRef-PACKAGE-cpython")
695695

696696
# The Windows embed artifacts don't contain pip/ensurepip,
697-
# but the MSI artifacts do. Add pip for MSI installers.
698-
if artifact_name.endswith(".exe"):
697+
# but the others do.
698+
if "-embed" not in artifact_name:
699699

700700
# Find the pip wheel in ensurepip in the source code
701701
for pathname in os.listdir(cpython_source_dir / "Lib/ensurepip/_bundled"):

windows-release/checkout.yml

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,27 @@
11
parameters:
22
depth: 3
3+
IncludeSelf: false
4+
Path: .
35

46
steps:
5-
- checkout: none
7+
- ${{ if eq(parameters.IncludeSelf, 'true') }}:
8+
- checkout: self
9+
- ${{ else }}:
10+
- checkout: none
611

7-
- script: git clone --progress -v --depth ${{ parameters.depth }} --branch $(SourceTag) --single-branch https://github.com/$(GitRemote)/cpython.git .
12+
- script: git clone --progress -v --depth ${{ parameters.depth }} --branch $(SourceTag) --single-branch https://github.com/$(GitRemote)/cpython.git ${{ parameters.Path }}
813
displayName: 'git clone ($(GitRemote)/$(SourceTag))'
914
condition: and(succeeded(), and(variables['GitRemote'], variables['SourceTag']))
1015

11-
- script: git clone --progress -v --depth ${{ parameters.depth }} --branch $(SourceTag) --single-branch $(Build.Repository.Uri) .
16+
- script: git clone --progress -v --depth ${{ parameters.depth }} --branch $(SourceTag) --single-branch $(Build.Repository.Uri) ${{ parameters.Path }}
1217
displayName: 'git clone (<default>/$(SourceTag))'
1318
condition: and(succeeded(), and(not(variables['GitRemote']), variables['SourceTag']))
1419

15-
- script: git clone --progress -v --depth ${{ parameters.depth }} --branch $(Build.SourceBranchName) --single-branch https://github.com/$(GitRemote)/cpython.git .
20+
- script: git clone --progress -v --depth ${{ parameters.depth }} --branch $(Build.SourceBranchName) --single-branch https://github.com/$(GitRemote)/cpython.git ${{ parameters.Path }}
1621
displayName: 'git clone ($(GitRemote)/<default>)'
1722
condition: and(succeeded(), and(variables['GitRemote'], not(variables['SourceTag'])))
1823

19-
- script: git clone --progress -v --depth ${{ parameters.depth }} --branch $(Build.SourceBranchName) --single-branch $(Build.Repository.Uri) .
24+
- script: git clone --progress -v --depth ${{ parameters.depth }} --branch $(Build.SourceBranchName) --single-branch $(Build.Repository.Uri) ${{ parameters.Path }}
2025
displayName: 'git clone'
2126
condition: and(succeeded(), and(not(variables['GitRemote']), not(variables['SourceTag'])))
2227

windows-release/merge-and-upload.py

Lines changed: 140 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,108 @@
77
from urllib.parse import urlparse
88
from urllib.request import urlopen, Request
99

10-
INDEX_PATH = os.getenv("INDEX_PATH", "ftp/downloads/__index_windows__.json")
11-
INDEX_URL = os.getenv("INDEX_URL") or f"https://www.python.org/{INDEX_PATH}"
10+
UPLOAD_URL_PREFIX = os.getenv("UPLOAD_URL_PREFIX") or "https://www.python.org/ftp/"
11+
UPLOAD_PATH_PREFIX = os.getenv("UPLOAD_PATH_PREFIX") or "/srv/www.python.org/ftp/"
12+
INDEX_URL = os.getenv("INDEX_URL") or f"https://www.python.org/ftp/python/__index_windows__.json"
1213
UPLOAD_HOST = os.getenv("PyDotOrgServer")
14+
UPLOAD_HOST_KEY = os.getenv("PyDotOrgHostKey")
15+
UPLOAD_KEYFILE = os.getenv("UPLOAD_KEYFILE")
16+
UPLOAD_USER = os.getenv("PyDotOrgUsername")
17+
NO_UPLOAD = os.getenv("NO_UPLOAD")
18+
19+
def find_cmd(env, exe):
20+
cmd = os.getenv(env)
21+
if cmd:
22+
return Path(cmd)
23+
for p in os.getenv("PATH", "").split(";"):
24+
if p:
25+
cmd = Path(p) / exe
26+
if cmd.is_file():
27+
return cmd
28+
if UPLOAD_HOST:
29+
raise RuntimeError(f"Could not find {exe} to perform upload. Try setting %{env}% or %PATH%")
30+
print(f"Did not find {exe}, but not uploading anyway.")
31+
32+
PLINK = find_cmd("PLINK", "plink.exe")
33+
PSCP = find_cmd("PSCP", "pscp.exe")
34+
35+
36+
def _std_args(cmd):
37+
if not cmd:
38+
raise RuntimeError("Cannot upload because command is missing")
39+
all_args = [cmd, "-batch"]
40+
if UPLOAD_HOST_KEY:
41+
all_args.append("-hostkey")
42+
all_args.append(UPLOAD_HOST_KEY)
43+
if UPLOAD_KEYFILE:
44+
all_args.append("-noagent")
45+
all_args.append("-i")
46+
all_args.append(UPLOAD_KEYFILE)
47+
return all_args
48+
49+
50+
class RunError(Exception):
51+
pass
52+
53+
54+
def _run(*args):
55+
with subprocess.Popen(
56+
args,
57+
stdout=subprocess.PIPE,
58+
stderr=subprocess.PIPE,
59+
encoding="ascii",
60+
errors="replace",
61+
) as p:
62+
out, err = p.communicate(None)
63+
if out:
64+
print(out)
65+
if err:
66+
print(err)
67+
if p.returncode:
68+
raise RunError(p.returncode, out, err)
69+
70+
71+
def call_ssh(*args, allow_fail=True):
72+
if not UPLOAD_HOST or NO_UPLOAD:
73+
print("Skipping", args, "because UPLOAD_HOST is missing")
74+
return
75+
try:
76+
_run(*_std_args(PLINK), f"{UPLOAD_USER}@{UPLOAD_HOST}", *args)
77+
except RunError:
78+
if not allow_fail:
79+
raise
80+
81+
82+
def upload_ssh(source, dest):
83+
if not UPLOAD_HOST or NO_UPLOAD:
84+
print("Skipping upload of", source, "because UPLOAD_HOST is missing")
85+
return
86+
_run(*_std_args(PSCP), source, f"{UPLOAD_USER}@{UPLOAD_HOST}:{dest}")
1387

1488

15-
def call_ssh(args):
89+
def download_ssh(source, dest):
1690
if not UPLOAD_HOST:
17-
print("Skipping", args, "because UPLOAD_HOST is missing")
91+
print("Skipping download of", source, "because UPLOAD_HOST is missing")
92+
return
93+
_run(*_std_args(PSCP), f"{UPLOAD_USER}@{UPLOAD_HOST}:{source}", dest)
94+
95+
96+
def ls_ssh(dest):
97+
if not UPLOAD_HOST:
98+
print("Skipping ls of", dest, "because UPLOAD_HOST is missing")
1899
return
19-
subprocess.check_output(args)
100+
try:
101+
_run(*_std_args(PSCP), "-ls", f"{UPLOAD_USER}@{UPLOAD_HOST}:{dest}")
102+
except RunError as ex:
103+
if not ex.args[2].rstrip().endswith("No such file or directory"):
104+
raise
105+
print(dest, "was not found")
106+
107+
108+
def url2path(url):
109+
if not url.startswith(UPLOAD_URL_PREFIX):
110+
raise ValueError(f"Unexpected URL: {url}")
111+
return UPLOAD_PATH_PREFIX + url[len(UPLOAD_URL_PREFIX):]
20112

21113

22114
def get_hashes(src):
@@ -30,7 +122,7 @@ def get_hashes(src):
30122

31123

32124
def purge(url):
33-
if not UPLOAD_HOST:
125+
if not UPLOAD_HOST or NO_UPLOAD:
34126
print("Skipping purge of", url, "because UPLOAD_HOST is missing")
35127
return
36128
with urlopen(Request(url, method="PURGE", headers={"Fastly-Soft-Purge": 1})) as r:
@@ -41,52 +133,73 @@ def calculate_uploads():
41133
for p in Path().absolute().glob("__install*.json"):
42134
i = json.loads(p.read_bytes())
43135
u = urlparse(i["url"])
136+
src = Path(u.path.rpartition("/")[-1]).absolute()
137+
dest = url2path(i["url"])
138+
sbom = src.with_suffix(".spdx.json")
139+
sbom_dest = dest.rpartition("/")[0] + sbom.name
140+
if not sbom.is_file():
141+
sbom = None
142+
sbom_dest = None
44143
yield (
45144
i,
46-
Path(u.path.rpartition("/")[-1]).absolute(),
47-
u.path,
145+
src,
146+
url2path(i["url"]),
147+
sbom,
148+
sbom_dest,
48149
)
49150

50-
def upload_files(uploads):
51-
for i, src, dest in uploads:
52-
print("Uploading", src, "to", dest)
53-
call_ssh([...])
54-
55-
def purge_files(uploads):
56-
for i, src, dest in uploads:
57-
purge(i["url"])
58151

59152
def hash_packages(uploads):
60-
for i, src, dest in uploads:
153+
for i, src, *_ in uploads:
61154
i["hashes"] = get_hashes(src)
62155

63156

64157
UPLOADS = list(calculate_uploads())
65158
hash_packages(UPLOADS)
66159

67160

161+
INDEX_PATH = url2path(INDEX_URL)
68162
try:
69-
with open("__index__.json", "rb") as f:
70-
index = json.load(f)
71-
except FileNotFoundError:
163+
download_ssh(INDEX_PATH, "__index__.json")
164+
except RunError as ex:
165+
err = ex.args[2]
166+
if not err.rstrip().endswith("no such file or directory"):
167+
raise
72168
index = {"versions": []}
169+
else:
170+
with open("__index__.json", "r", encoding="utf-8") as f:
171+
index = json.load(f)
73172

74173

75174
# TODO: Sort?
76-
index["versions"][:0] = [i[0] for i in UPLOADS]
175+
index["versions"][:0] = [i for i, *_ in UPLOADS]
176+
77177

78-
with open("__index__.json", "wb") as f:
178+
with open("__index__.json", "w", encoding="utf-8") as f:
79179
# Include an indent for sanity while testing.
80180
# We should probably remove it later for the size benefits.
81-
json.dump(f, index, indent=1)
181+
json.dump(index, f, indent=1)
82182

83183
print("Merged", len(UPLOADS), "entries")
84184

185+
85186
# Upload last to ensure we've got a valid index first
86-
upload_files(UPLOADS)
187+
for i, src, dest, sbom, sbom_dest in UPLOADS:
188+
print("Uploading", src, "to", dest)
189+
destdir = dest.rpartition("/")[0]
190+
call_ssh("mkdir", destdir, "&&", "chgrp", "downloads", destdir, "&&", "chmod", "a+rx", destdir)
191+
upload_ssh(src, dest)
192+
call_ssh("chgrp", "downloads", dest, "&&", "chmod", "g-x,o+r", dest)
193+
if sbom and sbom_dest:
194+
upload_ssh(sbom, sbom_dest)
195+
call_ssh("chgrp", "downloads", sbom_dest, "&&", "chmod", "g-x,o+r", sbom_dest)
196+
197+
198+
print("Uploading __index__.json to", INDEX_URL)
199+
upload_ssh("__index__.json", INDEX_PATH)
87200

88-
print("Uploading __index__.json to python.org")
89-
run_ssh([...])
90201

91-
purge_files(UPLOADS)
202+
print("Purging", len(UPLOADS), "uploaded files")
203+
for i, src, dest in UPLOADS:
204+
purge(i["url"])
92205
purge(INDEX_URL)

windows-release/stage-pack-pymanager.yml

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,10 @@ jobs:
3636
- arm64_t
3737

3838
steps:
39-
- checkout: none
39+
- template: ./checkout.yml
40+
parameters:
41+
IncludeSelf: true
42+
Path: $(Build.SourcesDirectory)\cpython
4043

4144
- ${{ each Name in variables.artifacts }}:
4245
- download: current
@@ -49,12 +52,19 @@ jobs:
4952
$filename = Split-Path -Left $install.url
5053
# Deliberately move the __install__.json out. We need it to publish, but
5154
# it doesn't belong in the package.
52-
mv $env:INSTALL_JSON $env:INSTALL_JSON
53-
Compress-Archive -CompressionLevel Optimal $env:LAYOUT "$(Build.ArtifactStagingDirectory)\$filename"
55+
mv "${env:LAYOUT}\__install__.json" $env:INSTALL_JSON
56+
Compress-Archive -CompressionLevel Optimal "${env:LAYOUT}\*" "$(Build.ArtifactStagingDirectory)\$filename"
5457
env:
5558
LAYOUT: $(Pipeline.Workspace)\layout_pymanager_${{ Name }}
5659
INSTALL_JSON: $(Build.ArtifactStagingDirectory)\__install__.${{ Name }}.json
5760
61+
- powershell: >
62+
& $(Python) "$(Build.SourcesDirectory)\release-tools\sbom.py"
63+
"--cpython-source-dir=$(Build.SourcesDirectory)\cpython"
64+
$(gci "$(Build.ArtifactStagingDirectory)\*.zip")
65+
workingDirectory: $(Build.BinariesDirectory)
66+
displayName: 'Create SBOMs for binaries'
67+
5868
- publish: '$(Build.ArtifactStagingDirectory)'
5969
artifact: pymanager
6070
displayName: 'Publish Artifact: pymanager'

0 commit comments

Comments
 (0)