Skip to content

Commit 7fa0d84

Browse files
authored
Fix custom scie_jump digest validation. (#180)
Fixes #179
1 parent b24450b commit 7fa0d84

File tree

9 files changed

+259
-20
lines changed

9 files changed

+259
-20
lines changed

CHANGES.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
# Release Notes
22

3+
## 0.15.2
4+
5+
This release fixes validation of custom `scie_jump`s with some portion of their digest filled out.
6+
Now, if you specify a `size` or a `fingerprint` or both for the `lift.scie_jump.digest`, the
7+
corresponding property of the downloaded `scie-jump` binary will be validated or else the scie
8+
build will fail with an informative message.
9+
310
## 0.15.1
411

512
This release fixes a bug present since the initial 0.1.0 release for `--hash shake_*`. The SHAKE

science/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,6 @@
33

44
from packaging.version import Version
55

6-
__version__ = "0.15.1"
6+
__version__ = "0.15.2"
77

88
VERSION = Version(__version__)

science/a_scie.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
@dataclass(frozen=True)
2121
class LoadResult(FetchResult):
2222
binary_name: str
23+
version: Version | None = None
2324

2425

2526
def load_project_release(
@@ -44,7 +45,9 @@ def load_project_release(
4445
executable=True,
4546
ttl=ttl,
4647
)
47-
return LoadResult(path=result.path, digest=result.digest, binary_name=qualified_binary_name)
48+
return LoadResult(
49+
path=result.path, digest=result.digest, binary_name=qualified_binary_name, version=version
50+
)
4851

4952

5053
def jump(

science/commands/build.py

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -42,14 +42,13 @@ def assemble_scies(
4242
native_jump_path = (a_scie.custom_jump(repo_path=use_jump) if use_jump else a_scie.jump()).path
4343

4444
scies = list[ScieAssembly]()
45-
for platform_spec, lift_manifest in lift.export_manifest(
46-
lift_config, application, dest_dir=dest_dir, platform_specs=platform_specs
45+
for platform_spec, lift_manifest, jump_path in lift.export_manifest(
46+
lift_config,
47+
application,
48+
dest_dir=dest_dir,
49+
platform_specs=platform_specs,
50+
use_jump=use_jump,
4751
):
48-
jump_path = (
49-
a_scie.custom_jump(repo_path=use_jump)
50-
if use_jump
51-
else a_scie.jump(specification=application.scie_jump, platform=platform_spec.platform)
52-
).path
5352
with temporary_directory("assemble") as build_dir:
5453
subprocess.run(
5554
args=[str(native_jump_path), "-sj", str(jump_path), lift_manifest],

science/commands/lift.py

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,8 @@ def export_manifest(
101101
dest_dir: Path,
102102
*,
103103
platform_specs: Iterable[PlatformSpec] | None = None,
104-
) -> Iterator[tuple[PlatformSpec, Path]]:
104+
use_jump: Path | None = None,
105+
) -> Iterator[tuple[PlatformSpec, Path, Path]]:
105106
app_info = AppInfo.assemble(lift_config.app_info)
106107

107108
for platform_spec in platform_specs or application.platform_specs:
@@ -232,14 +233,24 @@ def maybe_invert_lazy(file: File) -> File:
232233

233234
build_info = application.build_info if lift_config.include_provenance else None
234235

236+
load_result = (
237+
a_scie.custom_jump(repo_path=use_jump)
238+
if use_jump
239+
else a_scie.jump(specification=application.scie_jump, platform=platform_spec.platform)
240+
)
241+
if load_result.version:
242+
scie_jump = ScieJump(version=load_result.version, digest=load_result.digest)
243+
else:
244+
scie_jump = ScieJump()
245+
235246
with open(lift_manifest, "w") as lift_manifest_output:
236247
_emit_manifest(
237248
lift_manifest_output,
238249
name=application.name,
239250
description=application.description,
240251
load_dotenv=application.load_dotenv,
241252
base=application.base,
242-
scie_jump=application.scie_jump or ScieJump(),
253+
scie_jump=scie_jump,
243254
platform_spec=platform_spec,
244255
distributions=distributions,
245256
interpreter_groups=application.interpreter_groups,
@@ -250,7 +261,7 @@ def maybe_invert_lazy(file: File) -> File:
250261
build_info=build_info,
251262
app_info=app_info,
252263
)
253-
yield platform_spec, lift_manifest
264+
yield platform_spec, lift_manifest, load_result.path
254265

255266

256267
def _render_file(file: File) -> dict[str, Any]:

science/exe.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -782,7 +782,7 @@ def export(
782782
application = parse_application(lift_config, config)
783783
platform_info = PlatformInfo.create(application, use_suffix=use_platform_suffix)
784784
with temporary_directory("export") as td:
785-
for _, manifest_path in lift.export_manifest(
785+
for _, manifest_path, _ in lift.export_manifest(
786786
lift_config, application, dest_dir=td, platform_specs=lift_config.platform_specs
787787
):
788788
lift_manifest = dest_dir / (

science/hashing.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -51,8 +51,8 @@ def __getattr__(self, name: str) -> Any:
5151

5252
@documented_dataclass(frozen=True, alias="digest")
5353
class Digest:
54-
size: int
55-
fingerprint: Fingerprint
54+
size: int | None = None
55+
fingerprint: Fingerprint | None = None
5656

5757
@classmethod
5858
def hasher(cls, underlying: BinaryIO, algorithm: str = DEFAULT_ALGORITHM) -> BinaryHasher:
@@ -71,7 +71,7 @@ def hash(cls, path: Path, algorithm: str = DEFAULT_ALGORITHM) -> Digest:
7171

7272
@dataclass(frozen=True)
7373
class ExpectedDigest:
74-
fingerprint: Fingerprint
74+
fingerprint: Fingerprint | None = None
7575
algorithm: str = DEFAULT_ALGORITHM
7676
size: int | None = None
7777

@@ -86,7 +86,7 @@ def maybe_check_size(self, subject: str, actual_size: Callable[[], int]) -> None
8686
)
8787

8888
def check_fingerprint(self, subject: str, actual_fingerprint: Fingerprint) -> None:
89-
if self.fingerprint != actual_fingerprint:
89+
if self.fingerprint and self.fingerprint != actual_fingerprint:
9090
raise InputError(
9191
f"The {subject} has unexpected contents.\n"
9292
f"Expected {self.algorithm} digest:\n"

science/model.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -226,13 +226,13 @@ def id(self) -> str:
226226
def placeholder(self) -> str:
227227
return f"{{{self.id}}}"
228228

229-
def maybe_check_digest(self, path: Path):
229+
def maybe_check_digest(self, path: Path) -> None:
230230
if not self.digest:
231231
return
232232
if self.source and self.source.lazy:
233233
return
234234
expected_digest = ExpectedDigest(fingerprint=self.digest.fingerprint, size=self.digest.size)
235-
return expected_digest.check_path(path)
235+
expected_digest.check_path(path)
236236

237237

238238
class Url(str):

tests/test_exe.py

Lines changed: 220 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,14 @@
2121

2222
import pytest
2323
import toml
24+
from packaging.version import Version
2425
from pytest import TempPathFactory
2526
from testing import issue
2627

27-
from science import __version__
28+
from science import __version__, a_scie
2829
from science.config import parse_config_file
30+
from science.hashing import Digest, Fingerprint
31+
from science.model import ScieJump
2932
from science.os import IS_WINDOWS
3033
from science.platform import CURRENT_PLATFORM, CURRENT_PLATFORM_SPEC, LibC, Os, Platform
3134
from science.providers import PyPy
@@ -1211,3 +1214,219 @@ def scie_select(python) -> dict[str, Any]:
12111214
"debug": 1,
12121215
"free-threaded": 0,
12131216
} == scie_select("python3.14d")
1217+
1218+
1219+
def test_load_dotenv(tmp_path: Path, science_exe: Path) -> None:
1220+
dest = tmp_path / "dest"
1221+
chroot = tmp_path / "chroot"
1222+
chroot.mkdir(parents=True, exist_ok=True)
1223+
1224+
subprocess.run(
1225+
args=[str(science_exe), "lift", "build", "--dest-dir", str(dest), "-"],
1226+
input=dedent(
1227+
"""\
1228+
[lift]
1229+
name = "exe"
1230+
load_dotenv = true
1231+
1232+
[[lift.interpreters]]
1233+
id = "cpython"
1234+
provider = "PythonBuildStandalone"
1235+
release = "20251120"
1236+
version = "3.14"
1237+
flavor = "install_only_stripped"
1238+
1239+
[[lift.commands]]
1240+
exe = "#{cpython:python}"
1241+
args = ["-c", "import os; print(os.environ.get('SLARTIBARTFAST', '<unset>'))"]
1242+
"""
1243+
),
1244+
cwd=chroot,
1245+
stdout=subprocess.PIPE,
1246+
text=True,
1247+
check=True,
1248+
)
1249+
exe = dest / "exe"
1250+
assert (
1251+
"<unset>"
1252+
== subprocess.run(args=[exe], stdout=subprocess.PIPE, text=True, check=True).stdout.strip()
1253+
)
1254+
(dest / ".env").write_text("SLARTIBARTFAST=42")
1255+
assert (
1256+
"<unset>"
1257+
== subprocess.run(args=[exe], stdout=subprocess.PIPE, text=True, check=True).stdout.strip()
1258+
)
1259+
assert (
1260+
"42"
1261+
== subprocess.run(
1262+
args=[exe], stdout=subprocess.PIPE, text=True, check=True, cwd=dest
1263+
).stdout.strip()
1264+
)
1265+
1266+
1267+
@pytest.mark.parametrize("version", ["1.8.0", "1.8.1", "1.8.2"])
1268+
def test_custom_jump_nominal(tmp_path: Path, science_exe: Path, version: str) -> None:
1269+
dest = tmp_path / "dest"
1270+
chroot = tmp_path / "chroot"
1271+
chroot.mkdir(parents=True, exist_ok=True)
1272+
1273+
lift_manifest = chroot / "lift.toml"
1274+
lift_manifest.write_text(
1275+
dedent(
1276+
f"""\
1277+
[lift]
1278+
name = "exe"
1279+
1280+
[lift.scie_jump]
1281+
version = "{version}"
1282+
1283+
[[lift.interpreters]]
1284+
id = "cpython"
1285+
provider = "PythonBuildStandalone"
1286+
release = "20251120"
1287+
version = "3.14"
1288+
flavor = "install_only_stripped"
1289+
1290+
[[lift.commands]]
1291+
exe = "#{{cpython:python}}"
1292+
args = ["-V"]
1293+
"""
1294+
)
1295+
)
1296+
1297+
cache_dir = tmp_path / "cache"
1298+
subprocess.run(
1299+
args=[str(science_exe), "lift", "build", "--dest-dir", str(dest), lift_manifest],
1300+
env={**os.environ, "SCIENCE_CACHE_DIR": str(cache_dir)},
1301+
check=True,
1302+
)
1303+
exe = dest / "exe"
1304+
1305+
split_dir = tmp_path / "split"
1306+
subprocess.run(args=[exe, split_dir], env={**os.environ, "SCIE": "split"}, check=True)
1307+
assert (
1308+
version
1309+
== subprocess.run(
1310+
args=[split_dir / "scie-jump", "-V"], stdout=subprocess.PIPE, text=True, check=True
1311+
).stdout.strip()
1312+
)
1313+
1314+
result = subprocess.run(
1315+
args=[exe],
1316+
env={**os.environ, "SCIE": "inspect"},
1317+
stdout=subprocess.PIPE,
1318+
text=True,
1319+
check=True,
1320+
)
1321+
manifest = json.loads(result.stdout)
1322+
assert version == manifest["scie"]["jump"]["version"]
1323+
1324+
load_result = a_scie.jump(ScieJump(version=Version(version)))
1325+
assert load_result.digest.size == manifest["scie"]["jump"]["size"]
1326+
assert os.path.getsize(load_result.path) == manifest["scie"]["jump"]["size"]
1327+
1328+
1329+
GOOD_SIZE = 2223910
1330+
GOOD_FINGERPRINT = Fingerprint("e7ebc56578041eb5c92d819f948f9c8d5a671afaa337720d7d310f5311a2c5c3")
1331+
1332+
BAD_SIZE = -1
1333+
BAD_FINGERPRINT = Fingerprint("bad")
1334+
1335+
1336+
def digest_id(digest: Digest) -> str:
1337+
if digest.size and digest.fingerprint:
1338+
components = ["digest"]
1339+
if digest.size == BAD_SIZE:
1340+
components.append("bad-size")
1341+
if digest.fingerprint == BAD_FINGERPRINT:
1342+
components.append("bad-fingerprint")
1343+
return "-".join(components)
1344+
if digest.size:
1345+
return "bad-size" if digest.size == BAD_SIZE else "size"
1346+
if digest.fingerprint:
1347+
return "bad-fingerprint" if digest.fingerprint == BAD_FINGERPRINT else "fingerprint"
1348+
raise AssertionError(f"Expected digest to have at least one field set: {digest}")
1349+
1350+
1351+
@pytest.mark.parametrize(
1352+
"digest",
1353+
[
1354+
pytest.param(digest, id=digest_id(digest))
1355+
for digest in (
1356+
Digest(size=GOOD_SIZE, fingerprint=GOOD_FINGERPRINT),
1357+
Digest(size=BAD_SIZE, fingerprint=BAD_FINGERPRINT),
1358+
Digest(size=BAD_SIZE, fingerprint=GOOD_FINGERPRINT),
1359+
Digest(size=GOOD_SIZE, fingerprint=BAD_FINGERPRINT),
1360+
Digest(size=GOOD_SIZE),
1361+
Digest(size=BAD_SIZE),
1362+
Digest(fingerprint=GOOD_FINGERPRINT),
1363+
Digest(fingerprint=BAD_FINGERPRINT),
1364+
)
1365+
],
1366+
)
1367+
def test_custom_jump_invalid(tmp_path: Path, science_exe: Path, digest: Digest) -> None:
1368+
dest = tmp_path / "dest"
1369+
chroot = tmp_path / "chroot"
1370+
chroot.mkdir(parents=True, exist_ok=True)
1371+
1372+
digest_fields = []
1373+
if digest.size:
1374+
digest_fields.append(f"size = {digest.size}")
1375+
if digest.fingerprint:
1376+
digest_fields.append(f'fingerprint = "{digest.fingerprint}"')
1377+
assert digest_fields, f"Expected digest to have at least one field set; given: {digest}"
1378+
1379+
lift_manifest = chroot / "lift.toml"
1380+
lift_manifest.write_text(
1381+
dedent(
1382+
f"""\
1383+
[lift]
1384+
name = "exe"
1385+
platforms = [{{ platform = "linux-x86_64", libc = "gnu" }}]
1386+
1387+
[lift.scie_jump]
1388+
version = "1.8.2"
1389+
digest = {{ {", ".join(digest_fields)} }}
1390+
1391+
[[lift.interpreters]]
1392+
id = "cpython"
1393+
provider = "PythonBuildStandalone"
1394+
release = "20251120"
1395+
version = "3.14"
1396+
flavor = "install_only_stripped"
1397+
1398+
[[lift.commands]]
1399+
exe = "#{{cpython:python}}"
1400+
args = ["-V"]
1401+
"""
1402+
)
1403+
)
1404+
1405+
cache_dir = tmp_path / "cache"
1406+
result = subprocess.run(
1407+
args=[str(science_exe), "lift", "build", "--dest-dir", str(dest), lift_manifest],
1408+
env={**os.environ, "SCIENCE_CACHE_DIR": str(cache_dir)},
1409+
stderr=subprocess.PIPE,
1410+
text=True,
1411+
)
1412+
if digest.size == BAD_SIZE:
1413+
assert result.returncode != 0
1414+
assert (
1415+
"The content at "
1416+
"https://github.com/a-scie/jump/releases/download/v1.8.2/scie-jump-linux-x86_64 is "
1417+
f"expected to be {BAD_SIZE} bytes, but advertises a Content-Length of {GOOD_SIZE} "
1418+
"bytes."
1419+
) in result.stderr
1420+
elif digest.fingerprint == BAD_FINGERPRINT:
1421+
assert result.returncode != 0
1422+
assert (
1423+
"The download from "
1424+
"https://github.com/a-scie/jump/releases/download/v1.8.2/scie-jump-linux-x86_64 has "
1425+
"unexpected contents.\n"
1426+
"Expected sha256 digest:\n"
1427+
f" {BAD_FINGERPRINT}\n"
1428+
"Actual sha256 digest:\n"
1429+
f" {GOOD_FINGERPRINT}"
1430+
) in result.stderr
1431+
else:
1432+
assert 0 == result.returncode

0 commit comments

Comments
 (0)