From a0d989c3c9e48beea813faa0ee4526f504d25c64 Mon Sep 17 00:00:00 2001 From: Aleksei Menshutin Date: Thu, 15 Oct 2026 02:12:46 +0300 Subject: [PATCH 1/3] Add reproducible locked class project acquisition --- usvm-ts-calls/PROJECT_ACQUISITION.md | 62 ++++ .../scripts/acquire-class-projects.py | 287 ++++++++++++++++++ .../scripts/test_acquire_class_projects.py | 115 +++++++ 3 files changed, 464 insertions(+) create mode 100644 usvm-ts-calls/PROJECT_ACQUISITION.md create mode 100644 usvm-ts-calls/scripts/acquire-class-projects.py create mode 100644 usvm-ts-calls/scripts/test_acquire_class_projects.py diff --git a/usvm-ts-calls/PROJECT_ACQUISITION.md b/usvm-ts-calls/PROJECT_ACQUISITION.md new file mode 100644 index 0000000000..496abd7183 --- /dev/null +++ b/usvm-ts-calls/PROJECT_ACQUISITION.md @@ -0,0 +1,62 @@ +# Acquire frozen class projects + +Run `scripts/acquire-class-projects.py` before class startup or coverage measurement. +It reads the unchanged selection ledger, creates independent detached clones at +its exact original revisions, installs locked dependencies, and snapshots the +acquired bytes. It never edits the original cached checkouts. + +```sh +python3 usvm-ts-calls/scripts/acquire-class-projects.py \ + --ledger /absolute/path/selection-ledger.json \ + --output /absolute/path/new-acquisition-attempt \ + --jobs 3 +``` + +The output must be a new attempt directory. `--project-id` may select projects for +an explicit retry; keep previous receipts and include every original cohort class +in the final startup report. A failed installation is retained in `projects.json` +and its per-project `acquisition.json`; the process exits nonzero after collecting +all results. Command receipts preserve exit/timeout outcome and hashed logs. + +The declared `packageManager` version wins. Otherwise, use the unique original +lockfile, an explicit `--manager-version NAME=VERSION`, an exact corresponding +`engines` version, or the documented acquisition default in the script. Version +ranges are not interpreted as exact pins. Modern Yarn uses the immutable lock +and `--mode=skip-build`; npm, pnpm and Bun disable dependency lifecycle scripts. +The Bun bootstrap extracts only the ordinary binary from its pinned official +platform package and records package, archive and executable hashes. + +Generated assets may be acquired using an existing original package script: + +```sh +python3 usvm-ts-calls/scripts/acquire-class-projects.py \ + --ledger /absolute/path/selection-ledger.json \ + --output /absolute/path/new-acquisition-attempt \ + --project-id samuelmtimbo--unit \ + --build-script samuelmtimbo--unit=build:system +``` + +Only explicitly requested script names from the original `package.json` run. +They execute before measurement. Tracked source and lockfile changes fail the +attempt even when a package manager or build script reports success. Generated +untracked files and installed dependencies are captured in the snapshot. Symlinks +must resolve inside the acquired project root. + +`phase=FROZEN` proves acquisition and byte freezing. It deliberately leaves +`acquisitionComplete=false`: startup graph resolution must still prove the +consumed configuration/runtime graph and its explicit external boundaries. +The full installation snapshot is shared acquisition evidence, **not** a frontend +root list or a request to lower all devDependencies. A class startup manifest must +select the original target plus its actual runtime source closure, verify every +consumed hash against the snapshot, and record qualification separately. + +Installation, generated-asset builds, and package-manager bootstrap are prohibited +inside timed measurement. A missing package, certificate error, changed lock, +unsupported runtime or incomplete graph remains an explicit failure outcome. + +Focused CLI checks: + +```sh +python3 -m unittest discover -s usvm-ts-calls/scripts \ + -p test_acquire_class_projects.py -v +``` diff --git a/usvm-ts-calls/scripts/acquire-class-projects.py b/usvm-ts-calls/scripts/acquire-class-projects.py new file mode 100644 index 0000000000..63ed052da8 --- /dev/null +++ b/usvm-ts-calls/scripts/acquire-class-projects.py @@ -0,0 +1,287 @@ +#!/usr/bin/env python3 +"""Acquire original frozen class projects before measurement; never run from the machine loop.""" +import argparse +import concurrent.futures +import hashlib +import json +import os +import platform +from pathlib import Path +import re +import shutil +import subprocess +import time +import tarfile +import threading + + +DEFAULT_MANAGER_VERSIONS = {'npm': '11.17.0', 'pnpm': '10.17.1', 'yarn': '1.22.22', 'bun': '1.3.14'} +TOOL_LOCK = threading.Lock() + +LOCKFILES = {'npm': ['npm-shrinkwrap.json', 'package-lock.json'], 'pnpm': ['pnpm-lock.yaml'], + 'yarn': ['yarn.lock'], 'bun': ['bun.lock', 'bun.lockb']} + + +def sha256(data): + return hashlib.sha256(data).hexdigest() + + +def write_json(path, value): + path.parent.mkdir(parents=True, exist_ok=True) + temporary = path.with_suffix(path.suffix + '.tmp') + temporary.write_text(json.dumps(value, indent=2, sort_keys=True) + '\n', encoding='utf-8') + os.replace(temporary, path) + + +def git(root, *arguments): + return subprocess.check_output(['git', '-C', str(root), *arguments], stderr=subprocess.STDOUT) + + +def project_groups(ledger): + groups = {} + for case in ledger['decisions']: + revision = re.search(r'@([0-9a-f]{40}):', case['identity']).group(1) + key = case['projectId'] + group = groups.setdefault(key, {'projectId': key, 'sourceRoot': case['sourceRoot'], + 'projectRevision': revision, 'classes': []}) + if (group['sourceRoot'], group['projectRevision']) != (case['sourceRoot'], revision): + raise ValueError(f'Conflicting original source identity for {key}') + group['classes'].append({field: case[field] for field in ['identity', 'sourceFile', 'className']}) + return list(groups.values()) + + +def manager_for(root, overrides): + package = json.loads((root / 'package.json').read_text(encoding='utf-8')) + declared = package.get('packageManager') + if declared: + match = re.fullmatch(r'(npm|pnpm|yarn|bun)@(\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?)(?:\+.*)?', declared) + if not match: + raise ValueError(f'Unsupported declared package manager: {declared}') + manager, version = match.groups() + version_source = 'package.json#packageManager' + else: + managers = [name for name, locks in LOCKFILES.items() if any((root / lock).is_file() for lock in locks)] + if len(managers) != 1: + raise ValueError(f'Expected one declared lockfile manager, found {managers}') + manager = managers[0] + version = overrides.get(manager, DEFAULT_MANAGER_VERSIONS[manager]) + version_source = 'explicit override' if manager in overrides else 'pinned acquisition default' + engine = package.get('engines', {}).get(manager) + exact_engine = isinstance(engine, str) and re.fullmatch(r'\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?', engine) + if manager not in overrides and exact_engine: + version = engine + version_source = f'package.json#engines.{manager}' + if manager == 'yarn' and not exact_engine and '__metadata:' in (root / 'yarn.lock').read_text(encoding='utf-8'): + version = overrides.get('yarn', '4.9.0') + if not any((root / lock).is_file() for lock in LOCKFILES[manager]): + raise ValueError(f'No lockfile for declared manager {manager}') + return manager, version, version_source + + +def manager_command(manager, version, tools_root): + executable = shutil.which(manager) + if executable: + result = subprocess.run([executable, '--version'], capture_output=True, text=True, timeout=60) + if result.returncode == 0 and result.stdout.strip() == version: + return [executable] + if manager == 'bun': + return [str(acquire_bun_binary(version, tools_root))] + package = f'@yarnpkg/cli-dist@{version}' if manager == 'yarn' and not version.startswith('1.') else f'{manager}@{version}' + return ['npm', 'exec', '--yes', '--ignore-scripts', f'--package={package}', '--', manager] + + +def acquire_bun_binary(version, tools_root): + systems = {'Darwin': 'darwin', 'Linux': 'linux'} + architectures = {'arm64': 'aarch64', 'aarch64': 'aarch64', 'x86_64': 'x64', 'AMD64': 'x64'} + system, architecture = systems.get(platform.system()), architectures.get(platform.machine()) + if not system or not architecture: + raise ValueError('Unsupported platform for offline Bun binary acquisition') + directory = tools_root / f'bun-{version}-{system}-{architecture}' + executable = directory / 'bun' + with TOOL_LOCK: + if not executable.exists(): + directory.mkdir(parents=True, exist_ok=True) + package = f'@oven/bun-{system}-{architecture}@{version}' + command = ['npm', 'pack', '--ignore-scripts', '--json', '--pack-destination', str(directory), package] + result = subprocess.run(command, capture_output=True, text=True, timeout=300, check=True) + package_receipt = json.loads(result.stdout)[0] + archive = directory / package_receipt['filename'] + with tarfile.open(archive, 'r:gz') as tar: + member = tar.getmember('package/bin/bun') + if not member.isfile(): + raise ValueError('Bun package binary is not an ordinary file') + executable.write_bytes(tar.extractfile(member).read()) + executable.chmod(0o755) + write_json(directory / 'receipt.json', {'package': package, 'packageReceipt': package_receipt, + 'command': command, 'tarballSha256': sha256(archive.read_bytes()), + 'binarySha256': sha256(executable.read_bytes()), 'lifecycleScriptsExecuted': False}) + actual = subprocess.check_output([str(executable), '--version'], text=True).strip() + if actual != version: + raise ValueError(f'Acquired Bun version differs: {actual} != {version}') + return executable + + +def installation_arguments(manager, version): + if manager == 'npm': + return ['ci', '--ignore-scripts', '--no-audit', '--no-fund'] + if manager == 'pnpm': + return ['install', '--frozen-lockfile', '--ignore-scripts', '--ignore-pnpmfile'] + if manager == 'bun': + return ['install', '--frozen-lockfile', '--ignore-scripts'] + if version.startswith('1.'): + return ['install', '--frozen-lockfile', '--ignore-scripts', '--non-interactive'] + return ['install', '--immutable', '--mode=skip-build'] + + +def run_recorded(command, root, log, timeout): + started = time.monotonic() + timed_out = False + with log.open('wb') as output: + try: + result = subprocess.run(command, cwd=root, stdout=output, stderr=subprocess.STDOUT, timeout=timeout) + exit_code = result.returncode + except subprocess.TimeoutExpired: + timed_out = True + exit_code = None + record = {'command': command, 'exitCode': exit_code, 'timedOut': timed_out, + 'elapsedMillis': round((time.monotonic() - started) * 1000), + 'logSha256': sha256(log.read_bytes()), 'logPath': str(log)} + return record + + +def check_command(record): + if record['timedOut']: + raise RuntimeError(f"Acquisition command timed out; see {record['logPath']}") + if record['exitCode']: + raise RuntimeError(f"Acquisition command exited {record['exitCode']}; see {record['logPath']}") + + +def snapshot(root): + files, links = {}, {} + canonical_root = root.resolve() + for directory, directories, names in os.walk(root, followlinks=False): + directories[:] = sorted(name for name in directories if name != '.git') + for name in [*directories, *sorted(names)]: + path = Path(directory) / name + relative = path.relative_to(root).as_posix() + if path.is_symlink(): + target = path.resolve(strict=True) + if not target.is_relative_to(canonical_root): + raise ValueError(f'Acquired symlink escapes frozen root: {relative}') + links[relative] = target.relative_to(canonical_root).as_posix() + if path.is_file(): + files[relative] = sha256(path.read_bytes()) + identity = sha256(json.dumps({'files': files, 'links': links}, sort_keys=True, separators=(',', ':')).encode()) + return {'inputSha256': files, 'symlinks': links, 'snapshotSha256': identity} + + +def verify_snapshot(root, expected): + actual = snapshot(root) + if actual != expected: + raise ValueError('Acquired runtime/config/dependency snapshot changed') + + +def acquire_project(project, output, install, overrides, scripts, timeout): + destination = output / project['projectId'] + destination.mkdir(parents=True, exist_ok=False) + receipt_path = destination / 'acquisition.json' + checkout = destination / 'source' + original = Path(project['sourceRoot']).resolve() + receipt = {'schemaVersion': 1, **project, 'originalSourceRoot': str(original), 'sourceRoot': str(checkout), + 'phase': 'STARTED', 'commands': [], 'measurementMayRunInstall': False, + 'dependencyLifecycleScriptsExecuted': False, 'buildScripts': scripts} + write_json(receipt_path, receipt) + try: + revision = project['projectRevision'] + if git(original, 'rev-parse', 'HEAD').decode().strip() != revision: + raise ValueError('Original checkout does not match the frozen revision') + git(original, 'diff', '--quiet', 'HEAD', '--') + originals = {case['sourceFile']: sha256(git(original, 'show', f"{revision}:{case['sourceFile']}")) + for case in project['classes']} + subprocess.run(['git', 'clone', '--no-hardlinks', '--no-checkout', str(original), str(checkout)], + check=True, stdout=subprocess.DEVNULL, stderr=subprocess.PIPE) + git(checkout, 'checkout', '--quiet', '--detach', revision) + receipt['phase'] = 'CLONED' + receipt['nodeVersion'] = subprocess.check_output(['node', '--version'], text=True).strip() + manager, version, version_source = manager_for(checkout, overrides) + receipt['packageManager'] = {'name': manager, 'version': version, 'versionSource': version_source} + command = manager_command(manager, version, tools_root=output / ".tools") if install or scripts else None + if install: + receipt['commands'].append(run_recorded(command + installation_arguments(manager, version), + checkout, destination / 'install.log', timeout)) + check_command(receipt['commands'][-1]) + receipt['phase'] = 'INSTALLED' + package = json.loads((checkout / 'package.json').read_text(encoding='utf-8')) + for index, script in enumerate(scripts): + if script not in package.get('scripts', {}): + raise ValueError(f'No original package script named {script}') + receipt['commands'].append(run_recorded(command + ['run', script], checkout, + destination / f'build-{index}.log', timeout)) + check_command(receipt['commands'][-1]) + # Builds may add ignored/generated files, but they cannot silently alter original source. + git(checkout, 'diff', '--quiet', 'HEAD', '--') + for relative, expected in originals.items(): + if sha256((checkout / relative).read_bytes()) != expected: + raise ValueError(f'Original class source changed during acquisition: {relative}') + receipt['originalClassSha256'] = originals + receipt['snapshot'] = snapshot(checkout) + receipt['phase'] = 'FROZEN' + receipt['dependenciesInstalled'] = install + # A frozen installation is acquisition evidence. Startup graph resolution separately proves + # that every consumed module and explicit external runtime boundary was captured. + receipt['acquisitionComplete'] = False + except (OSError, ValueError, RuntimeError, subprocess.SubprocessError) as error: + receipt['failedPhase'] = receipt['phase'] + receipt['phase'] = 'FAILED' + receipt['diagnostic'] = str(error) + write_json(receipt_path, receipt) + return {'projectId': project['projectId'], 'phase': receipt['phase'], 'sourceRoot': str(checkout), + 'classCount': len(project['classes']), 'receiptPath': str(receipt_path), + 'acquisitionSha256': sha256(receipt_path.read_bytes()), 'diagnostic': receipt.get('diagnostic')} + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--ledger', required=True, type=Path) + parser.add_argument('--output', required=True, type=Path) + parser.add_argument('--project-id', action='append', default=[]) + parser.add_argument('--skip-install', action='store_true') + parser.add_argument('--manager-version', action='append', default=[], metavar='NAME=VERSION') + parser.add_argument('--build-script', action='append', default=[], metavar='PROJECT=SCRIPT') + parser.add_argument('--jobs', type=int, default=3) + parser.add_argument('--timeout-seconds', type=int, default=1800) + arguments = parser.parse_args() + ledger_bytes = arguments.ledger.read_bytes() + projects = project_groups(json.loads(ledger_bytes)) + if arguments.project_id: + projects = [project for project in projects if project['projectId'] in arguments.project_id] + if len(projects) != len(set(arguments.project_id)): + parser.error('Unknown or duplicate project selection') + if arguments.jobs < 1 or arguments.timeout_seconds < 1: + parser.error('Jobs and timeout must be positive') + overrides = dict(value.split('=', 1) for value in arguments.manager_version) + for manager, version in overrides.items(): + if manager not in DEFAULT_MANAGER_VERSIONS or not re.fullmatch(r'\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?', version): + parser.error('Manager overrides must name a supported manager and an exact version') + scripts = {} + for value in arguments.build_script: + project, script = value.split('=', 1) + scripts.setdefault(project, []).append(script) + output = arguments.output.resolve() + output.mkdir(parents=True, exist_ok=True) + results = [] + with concurrent.futures.ThreadPoolExecutor(max_workers=arguments.jobs) as pool: + futures = [pool.submit(acquire_project, project, output, not arguments.skip_install, overrides, + scripts.get(project['projectId'], []), arguments.timeout_seconds) for project in projects] + for future in concurrent.futures.as_completed(futures): + result = future.result() + results.append(result) + print(json.dumps(result), flush=True) + write_json(output / 'projects.json', {'schemaVersion': 1, 'ledgerSha256': sha256(ledger_bytes), + 'classCount': sum(item['classCount'] for item in results), + 'projects': sorted(results, key=lambda item: item['projectId'])}) + return 1 if any(result['phase'] == 'FAILED' for result in results) else 0 + + +if __name__ == '__main__': + raise SystemExit(main()) diff --git a/usvm-ts-calls/scripts/test_acquire_class_projects.py b/usvm-ts-calls/scripts/test_acquire_class_projects.py new file mode 100644 index 0000000000..7554e44763 --- /dev/null +++ b/usvm-ts-calls/scripts/test_acquire_class_projects.py @@ -0,0 +1,115 @@ +import importlib.util +import json +from pathlib import Path +import subprocess +import tempfile +import unittest +from unittest.mock import patch + +MODULE_PATH = Path(__file__).with_name('acquire-class-projects.py') +SPEC = importlib.util.spec_from_file_location('acquisition', MODULE_PATH) +acquisition = importlib.util.module_from_spec(SPEC) +SPEC.loader.exec_module(acquisition) + + +class AcquisitionTest(unittest.TestCase): + def test_original_git_source_clone_and_snapshot_are_independent(self): + with tempfile.TemporaryDirectory(prefix='calls-acquire-') as temporary: + directory = Path(temporary) + original = directory / 'original' + original.mkdir() + (original / 'Entry.ts').write_text('export class Entry { read() { return 1; } }\n') + (original / 'package.json').write_text('{"name":"source","version":"1.0.0"}') + (original / 'package-lock.json').write_text('{"lockfileVersion":3}') + subprocess.run(['git', 'init', '--quiet', str(original)], check=True) + acquisition.git(original, 'add', '--', 'Entry.ts', 'package.json', 'package-lock.json') + acquisition.git(original, '-c', 'user.name=Acquisition Test', '-c', 'user.email=test@example.invalid', + '-c', 'commit.gpgsign=false', 'commit', '--quiet', '-m', 'Freeze source') + revision = acquisition.git(original, 'rev-parse', 'HEAD').decode().strip() + project = {'projectId': 'example', 'sourceRoot': str(original), 'projectRevision': revision, + 'classes': [{'identity': f'example@{revision}:Entry.ts#Entry', + 'sourceFile': 'Entry.ts', 'className': 'Entry'}]} + output = directory / 'acquired' + output.mkdir() + + result = acquisition.acquire_project(project, output, install=False, overrides={}, scripts=[], timeout=30) + receipt = json.loads(Path(result['receiptPath']).read_text()) + clone = Path(result['sourceRoot']) + + self.assertEqual('FROZEN', result['phase']) + self.assertEqual((original / 'Entry.ts').read_bytes(), (clone / 'Entry.ts').read_bytes()) + self.assertFalse(receipt['dependencyLifecycleScriptsExecuted']) + self.assertFalse(receipt['acquisitionComplete']) + acquisition.verify_snapshot(clone, receipt['snapshot']) + (clone / 'Entry.ts').write_text('export class Entry { read() { return 2; } }\n') + with self.assertRaisesRegex(ValueError, 'snapshot changed'): + acquisition.verify_snapshot(clone, receipt['snapshot']) + self.assertEqual('export class Entry { read() { return 1; } }\n', (original / 'Entry.ts').read_text()) + + def test_each_installation_uses_the_lock_and_disables_dependency_scripts(self): + for manager, version, expected in [('npm', '11.17.0', '--ignore-scripts'), + ('pnpm', '10.17.1', '--ignore-scripts'), + ('bun', '1.3.14', '--ignore-scripts'), + ('yarn', '1.22.22', '--ignore-scripts'), + ('yarn', '4.9.0', '--mode=skip-build')]: + with self.subTest(manager=manager, version=version): + arguments = acquisition.installation_arguments(manager, version) + self.assertIn(expected, arguments) + self.assertTrue(arguments[0] == 'ci' or '--frozen-lockfile' in arguments or '--immutable' in arguments) + + def test_snapshot_captures_in_root_workspace_links_and_rejects_escaping_links(self): + with tempfile.TemporaryDirectory(prefix='calls-snapshot-') as temporary: + root = Path(temporary) + (root / 'packages').mkdir() + (root / 'packages/helper.ts').write_text('export const value = 3;') + (root / 'helper.ts').symlink_to(root / 'packages/helper.ts') + + snapshot = acquisition.snapshot(root) + + self.assertEqual('packages/helper.ts', snapshot['symlinks']['helper.ts']) + self.assertEqual(snapshot['inputSha256']['packages/helper.ts'], snapshot['inputSha256']['helper.ts']) + (root / 'outside').symlink_to(root.parent) + with self.assertRaisesRegex(ValueError, 'escapes frozen root'): + acquisition.snapshot(root) + + def test_exact_engine_version_is_used_for_the_inferred_manager(self): + with tempfile.TemporaryDirectory(prefix='calls-engine-') as temporary: + root = Path(temporary) + (root / 'package.json').write_text('{"engines":{"bun":"1.4.2"}}') + (root / 'bun.lock').write_text('{"lockfileVersion":2}') + + self.assertEqual(('bun', '1.4.2', 'package.json#engines.bun'), + acquisition.manager_for(root, overrides={})) + self.assertEqual(('bun', '1.3.14', 'explicit override'), + acquisition.manager_for(root, overrides={'bun': '1.3.14'})) + + def test_failed_commands_keep_the_command_log_and_failure_outcome(self): + with tempfile.TemporaryDirectory(prefix='calls-command-') as temporary: + root = Path(temporary) + log = root / 'failure.log' + + record = acquisition.run_recorded(['node', '-e', 'console.log("failure evidence"); process.exit(3)'], + root, log, timeout=30) + + self.assertEqual(3, record['exitCode']) + self.assertFalse(record['timedOut']) + self.assertEqual(acquisition.sha256(log.read_bytes()), record['logSha256']) + self.assertIn('failure evidence', log.read_text()) + with self.assertRaisesRegex(RuntimeError, 'exited 3'): + acquisition.check_command(record) + + def test_declared_package_manager_wins_over_inferred_lock_and_is_pinned(self): + with tempfile.TemporaryDirectory(prefix='calls-manager-') as temporary: + root = Path(temporary) + (root / 'package.json').write_text('{"packageManager":"pnpm@10.12.1"}') + (root / 'pnpm-lock.yaml').write_text('lockfileVersion: 9.0\n') + + self.assertEqual(('pnpm', '10.12.1', 'package.json#packageManager'), + acquisition.manager_for(root, overrides={'pnpm': '10.17.1'})) + (root / 'package.json').write_text('{"packageManager":"pnpm@latest"}') + with self.assertRaisesRegex(ValueError, 'Unsupported declared'): + acquisition.manager_for(root, overrides={}) + + +if __name__ == '__main__': + unittest.main() From dc62b948edb431b3d26b159a834e728e06c9408a Mon Sep 17 00:00:00 2001 From: Aleksei Menshutin Date: Thu, 15 Oct 2026 03:01:11 +0300 Subject: [PATCH 2/3] Acquire runtime assets with original workspace build recipes --- usvm-ts-calls/PROJECT_ACQUISITION.md | 3 +++ .../scripts/acquire-class-projects.py | 26 ++++++++++++++----- .../scripts/test_acquire_class_projects.py | 17 +++++++++++- 3 files changed, 38 insertions(+), 8 deletions(-) diff --git a/usvm-ts-calls/PROJECT_ACQUISITION.md b/usvm-ts-calls/PROJECT_ACQUISITION.md index 496abd7183..3d7bce2f53 100644 --- a/usvm-ts-calls/PROJECT_ACQUISITION.md +++ b/usvm-ts-calls/PROJECT_ACQUISITION.md @@ -37,6 +37,9 @@ python3 usvm-ts-calls/scripts/acquire-class-projects.py \ ``` Only explicitly requested script names from the original `package.json` run. +For an original workspace recipe, use `PROJECT=packages/helper#build`; the command +runs in that in-root workspace and records its working directory. Missing scripts +and escaping workspace paths fail rather than inventing or editing package scripts. They execute before measurement. Tracked source and lockfile changes fail the attempt even when a package manager or build script reports success. Generated untracked files and installed dependencies are captured in the snapshot. Symlinks diff --git a/usvm-ts-calls/scripts/acquire-class-projects.py b/usvm-ts-calls/scripts/acquire-class-projects.py index 63ed052da8..69f9e8baea 100644 --- a/usvm-ts-calls/scripts/acquire-class-projects.py +++ b/usvm-ts-calls/scripts/acquire-class-projects.py @@ -145,7 +145,7 @@ def run_recorded(command, root, log, timeout): exit_code = None record = {'command': command, 'exitCode': exit_code, 'timedOut': timed_out, 'elapsedMillis': round((time.monotonic() - started) * 1000), - 'logSha256': sha256(log.read_bytes()), 'logPath': str(log)} + 'logSha256': sha256(log.read_bytes()), 'logPath': str(log), 'workingDirectory': str(root)} return record @@ -181,6 +181,20 @@ def verify_snapshot(root, expected): raise ValueError('Acquired runtime/config/dependency snapshot changed') +def original_build_script(root, specification): + directory_name, script = specification.split('#', 1) if '#' in specification else ('.', specification) + relative = Path(directory_name) + if relative.is_absolute() or '..' in relative.parts: + raise ValueError('Build script directory escapes original project') + directory = (root / relative).resolve() + if not directory.is_relative_to(root.resolve()): + raise ValueError('Build script directory escapes original project') + package = json.loads((directory / 'package.json').read_text(encoding='utf-8')) + if script not in package.get('scripts', {}): + raise ValueError(f'No original package script named {script} in {directory_name}') + return directory, script + + def acquire_project(project, output, install, overrides, scripts, timeout): destination = output / project['projectId'] destination.mkdir(parents=True, exist_ok=False) @@ -211,11 +225,9 @@ def acquire_project(project, output, install, overrides, scripts, timeout): checkout, destination / 'install.log', timeout)) check_command(receipt['commands'][-1]) receipt['phase'] = 'INSTALLED' - package = json.loads((checkout / 'package.json').read_text(encoding='utf-8')) - for index, script in enumerate(scripts): - if script not in package.get('scripts', {}): - raise ValueError(f'No original package script named {script}') - receipt['commands'].append(run_recorded(command + ['run', script], checkout, + for index, specification in enumerate(scripts): + directory, script = original_build_script(checkout, specification) + receipt['commands'].append(run_recorded(command + ['run', script], directory, destination / f'build-{index}.log', timeout)) check_command(receipt['commands'][-1]) # Builds may add ignored/generated files, but they cannot silently alter original source. @@ -247,7 +259,7 @@ def main(): parser.add_argument('--project-id', action='append', default=[]) parser.add_argument('--skip-install', action='store_true') parser.add_argument('--manager-version', action='append', default=[], metavar='NAME=VERSION') - parser.add_argument('--build-script', action='append', default=[], metavar='PROJECT=SCRIPT') + parser.add_argument('--build-script', action='append', default=[], metavar='PROJECT=[DIRECTORY#]SCRIPT') parser.add_argument('--jobs', type=int, default=3) parser.add_argument('--timeout-seconds', type=int, default=1800) arguments = parser.parse_args() diff --git a/usvm-ts-calls/scripts/test_acquire_class_projects.py b/usvm-ts-calls/scripts/test_acquire_class_projects.py index 7554e44763..677bb06667 100644 --- a/usvm-ts-calls/scripts/test_acquire_class_projects.py +++ b/usvm-ts-calls/scripts/test_acquire_class_projects.py @@ -4,7 +4,6 @@ import subprocess import tempfile import unittest -from unittest.mock import patch MODULE_PATH = Path(__file__).with_name('acquire-class-projects.py') SPEC = importlib.util.spec_from_file_location('acquisition', MODULE_PATH) @@ -98,6 +97,22 @@ def test_failed_commands_keep_the_command_log_and_failure_outcome(self): with self.assertRaisesRegex(RuntimeError, 'exited 3'): acquisition.check_command(record) + def test_workspace_build_requires_an_original_script_inside_the_project(self): + with tempfile.TemporaryDirectory(prefix='calls-workspace-build-') as temporary: + root = Path(temporary).resolve() + package = root / 'packages/helper' + package.mkdir(parents=True) + (package / 'package.json').write_text('{"scripts":{"build":"tsc"}}') + + directory, script = acquisition.original_build_script(root, 'packages/helper#build') + + self.assertEqual(package, directory) + self.assertEqual('build', script) + with self.assertRaisesRegex(ValueError, 'No original package script'): + acquisition.original_build_script(root, 'packages/helper#publish') + with self.assertRaisesRegex(ValueError, 'escapes'): + acquisition.original_build_script(root, '../other#build') + def test_declared_package_manager_wins_over_inferred_lock_and_is_pinned(self): with tempfile.TemporaryDirectory(prefix='calls-manager-') as temporary: root = Path(temporary) From bb1f5a75dd1f30f21708a2257e60a871b815e941 Mon Sep 17 00:00:00 2001 From: Aleksei Menshutin Date: Thu, 15 Oct 2026 03:12:41 +0300 Subject: [PATCH 3/3] Forward explicit arguments to original workspace build scripts --- usvm-ts-calls/PROJECT_ACQUISITION.md | 4 +++- usvm-ts-calls/scripts/acquire-class-projects.py | 13 +++++++++---- .../scripts/test_acquire_class_projects.py | 3 ++- 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/usvm-ts-calls/PROJECT_ACQUISITION.md b/usvm-ts-calls/PROJECT_ACQUISITION.md index 3d7bce2f53..1f78475dc1 100644 --- a/usvm-ts-calls/PROJECT_ACQUISITION.md +++ b/usvm-ts-calls/PROJECT_ACQUISITION.md @@ -38,7 +38,9 @@ python3 usvm-ts-calls/scripts/acquire-class-projects.py \ Only explicitly requested script names from the original `package.json` run. For an original workspace recipe, use `PROJECT=packages/helper#build`; the command -runs in that in-root workspace and records its working directory. Missing scripts +runs in that in-root workspace and records its working directory. Arguments can +follow the declared script, for example `PROJECT=packages/vest-utils#build --package vest-utils`; +they are forwarded as command arguments rather than executed as a separate command. Missing scripts and escaping workspace paths fail rather than inventing or editing package scripts. They execute before measurement. Tracked source and lockfile changes fail the attempt even when a package manager or build script reports success. Generated diff --git a/usvm-ts-calls/scripts/acquire-class-projects.py b/usvm-ts-calls/scripts/acquire-class-projects.py index 69f9e8baea..83545555b3 100644 --- a/usvm-ts-calls/scripts/acquire-class-projects.py +++ b/usvm-ts-calls/scripts/acquire-class-projects.py @@ -9,6 +9,7 @@ from pathlib import Path import re import shutil +import shlex import subprocess import time import tarfile @@ -182,7 +183,11 @@ def verify_snapshot(root, expected): def original_build_script(root, specification): - directory_name, script = specification.split('#', 1) if '#' in specification else ('.', specification) + parts = shlex.split(specification) + if not parts: + raise ValueError('Expected an original package build script') + selected = parts[0] + directory_name, script = selected.split('#', 1) if '#' in selected else ('.', selected) relative = Path(directory_name) if relative.is_absolute() or '..' in relative.parts: raise ValueError('Build script directory escapes original project') @@ -192,7 +197,7 @@ def original_build_script(root, specification): package = json.loads((directory / 'package.json').read_text(encoding='utf-8')) if script not in package.get('scripts', {}): raise ValueError(f'No original package script named {script} in {directory_name}') - return directory, script + return directory, script, parts[1:] def acquire_project(project, output, install, overrides, scripts, timeout): @@ -226,8 +231,8 @@ def acquire_project(project, output, install, overrides, scripts, timeout): check_command(receipt['commands'][-1]) receipt['phase'] = 'INSTALLED' for index, specification in enumerate(scripts): - directory, script = original_build_script(checkout, specification) - receipt['commands'].append(run_recorded(command + ['run', script], directory, + directory, script, script_arguments = original_build_script(checkout, specification) + receipt['commands'].append(run_recorded(command + ['run', script, *script_arguments], directory, destination / f'build-{index}.log', timeout)) check_command(receipt['commands'][-1]) # Builds may add ignored/generated files, but they cannot silently alter original source. diff --git a/usvm-ts-calls/scripts/test_acquire_class_projects.py b/usvm-ts-calls/scripts/test_acquire_class_projects.py index 677bb06667..5e7fdd4330 100644 --- a/usvm-ts-calls/scripts/test_acquire_class_projects.py +++ b/usvm-ts-calls/scripts/test_acquire_class_projects.py @@ -104,10 +104,11 @@ def test_workspace_build_requires_an_original_script_inside_the_project(self): package.mkdir(parents=True) (package / 'package.json').write_text('{"scripts":{"build":"tsc"}}') - directory, script = acquisition.original_build_script(root, 'packages/helper#build') + directory, script, arguments = acquisition.original_build_script(root, 'packages/helper#build --package helper') self.assertEqual(package, directory) self.assertEqual('build', script) + self.assertEqual(['--package', 'helper'], arguments) with self.assertRaisesRegex(ValueError, 'No original package script'): acquisition.original_build_script(root, 'packages/helper#publish') with self.assertRaisesRegex(ValueError, 'escapes'):