diff --git a/scripts/publish_release.py b/scripts/publish_release.py index 5d552399..1ef1829d 100644 --- a/scripts/publish_release.py +++ b/scripts/publish_release.py @@ -235,11 +235,50 @@ def version_record(self, crate: str, version: str) -> RegistryRecord: def package_checksums( - manifest: ReleaseManifest, version: str + manifest: ReleaseManifest, version: str, metadata: Mapping[str, Any] ) -> dict[str, str]: + raw_packages = metadata.get("packages") + if not isinstance(raw_packages, list): + raise PublishError("cargo metadata packages must be an array") + packages = { + _string(package.get("name"), "cargo metadata package.name"): package + for raw_package in raw_packages + for package in (_object(raw_package, "cargo metadata package"),) + } + root = ROOT.resolve() + patch_arguments: list[str] = [] + for crate in manifest.ordered_crates: + package = packages.get(crate) + if package is None: + raise PublishError(f"cargo metadata is missing release package {crate}") + manifest_path = pathlib.Path( + _string(package.get("manifest_path"), f"{crate}.manifest_path") + ).resolve() + package_root = manifest_path.parent + try: + package_root.relative_to(root) + except ValueError: + raise PublishError( + f"release package {crate} is outside the workspace root" + ) from None + patch_arguments.extend( + [ + "--config", + f"patch.crates-io.{crate}.path={json.dumps(str(package_root))}", + ] + ) + checksums: dict[str, str] = {} for crate in manifest.ordered_crates: - command = ["cargo", "package", "--locked", "--no-verify", "-p", crate] + command = [ + "cargo", + "package", + "--locked", + "--no-verify", + "-p", + crate, + *patch_arguments, + ] result = subprocess.run( command, cwd=ROOT, check=False, capture_output=True, text=True ) @@ -457,8 +496,9 @@ def _allow_published_rerun() -> bool: def run(command: str, manifest_path: pathlib.Path) -> None: manifest = load_release_manifest(manifest_path) - version = validate_release_graph(manifest, cargo_metadata()) - checksums = package_checksums(manifest, version) + metadata = cargo_metadata() + version = validate_release_graph(manifest, metadata) + checksums = package_checksums(manifest, version, metadata) if command == "preflight": print( f"packaged {len(checksums)} crates for {version}; release manifest is valid" diff --git a/scripts/tests/test_publish_release.py b/scripts/tests/test_publish_release.py index 400c2849..d1c8b840 100644 --- a/scripts/tests/test_publish_release.py +++ b/scripts/tests/test_publish_release.py @@ -3,8 +3,11 @@ from __future__ import annotations import subprocess +import tempfile import unittest import urllib.error +from pathlib import Path +from unittest import mock from scripts import publish_release @@ -63,6 +66,55 @@ def test_metadata_validation_requires_complete_dependency_ordered_publish_set(se publish_release.validate_release_graph(incomplete, metadata) +class PackagingTests(unittest.TestCase): + def test_workspace_packages_resolve_unpublished_exact_dependencies_locally(self) -> None: + manifest = publish_release.ReleaseManifest( + ordered_crates=("base", "consumer"), + registry_independent=frozenset({"base"}), + ) + with tempfile.TemporaryDirectory() as temporary_directory: + root = Path(temporary_directory) + metadata = { + "packages": [ + { + "name": crate, + "manifest_path": str(root / "crates" / crate / "Cargo.toml"), + } + for crate in manifest.ordered_crates + ] + } + package_dir = root / "target" / "package" + package_dir.mkdir(parents=True) + for crate in manifest.ordered_crates: + (package_dir / f"{crate}-0.7.3.crate").write_bytes(crate.encode()) + + commands: list[list[str]] = [] + + def run(command: list[str], **kwargs: object) -> subprocess.CompletedProcess[str]: + commands.append(command) + self.assertEqual(kwargs["cwd"], root) + return subprocess.CompletedProcess(command, 0, "", "") + + with ( + mock.patch.object(publish_release, "ROOT", root), + mock.patch.object(publish_release.subprocess, "run", side_effect=run), + ): + publish_release.package_checksums(manifest, "0.7.3", metadata) + + expected_patches = { + f'patch.crates-io.{crate}.path="{root.resolve() / "crates" / crate}"' + for crate in manifest.ordered_crates + } + self.assertEqual(len(commands), 2) + for command in commands: + configs = { + command[index + 1] + for index, argument in enumerate(command) + if argument == "--config" + } + self.assertEqual(configs, expected_patches) + + class RegistryTests(unittest.TestCase): def test_registry_transport_errors_are_typed_and_retried(self) -> None: attempts = 0