Skip to content

Commit 246a2d8

Browse files
committed
Post review improvements
- save crates to the correct deps/cargo path. - add .cargo/config.toml as project file - format crates as a folder with sha's for compatibility with cargo vendor (and thus removing the need to use crates indexes)
1 parent c1b3940 commit 246a2d8

File tree

2 files changed

+149
-53
lines changed

2 files changed

+149
-53
lines changed

cachi2/core/package_managers/cargo.py

Lines changed: 67 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
11
# SPDX-License-Identifier: GPL-3.0-or-later
2+
import hashlib
3+
import json
24
import logging
5+
import tarfile
36
from pathlib import Path
7+
from textwrap import dedent
48

59
import tomli
610

711
from cachi2.core.checksum import ChecksumInfo, must_match_any_checksum
812
from cachi2.core.models.input import Request
9-
from cachi2.core.models.output import Component, RequestOutput
13+
from cachi2.core.models.output import Component, ProjectFile, RequestOutput
1014
from cachi2.core.package_managers.general import download_binary_file
1115
from cachi2.core.rooted_path import RootedPath
1216

@@ -32,10 +36,23 @@ def fetch_cargo_source(request: Request) -> RequestOutput:
3236
for dependency in info["dependencies"]:
3337
components.append(Component.from_package_dict(dependency))
3438

39+
cargo_config = ProjectFile(
40+
abspath=request.source_dir.join_within_root(".cargo/config.toml"),
41+
template=dedent(
42+
"""
43+
[source.crates-io]
44+
replace-with = "local"
45+
46+
[source.local]
47+
directory = "${output_dir}/deps/cargo"
48+
"""
49+
),
50+
)
51+
3552
return RequestOutput.from_obj_list(
3653
components=components,
3754
environment_variables=[],
38-
project_files=[],
55+
project_files=[cargo_config],
3956
)
4057

4158

@@ -50,8 +67,7 @@ def _resolve_cargo(
5067
assert pkg_name and pkg_version, "INVALID PACKAGE"
5168

5269
dependencies = []
53-
if not lock_file:
54-
lock_file = app_path / DEFAULT_LOCK_FILE
70+
lock_file = app_path / (lock_file or DEFAULT_LOCK_FILE)
5571

5672
cargo_lock_dict = tomli.load(lock_file.open("rb"))
5773
for dependency in cargo_lock_dict["package"]:
@@ -80,19 +96,63 @@ def _download_cargo_dependencies(output_path: RootedPath, cargo_dependencies: li
8096
checksum_info = ChecksumInfo(algorithm="sha256", hexdigest=dep["checksum"])
8197
dep_name = dep["name"]
8298
dep_version = dep["version"]
83-
download_path = Path(output_path.join_within_root(f"{dep_name}-{dep_version}.crate"))
84-
download_path.parent.mkdir(exist_ok=True)
99+
download_path = Path(
100+
output_path.join_within_root(f"deps/cargo/{dep_name}-{dep_version}.crate")
101+
)
102+
download_path.parent.mkdir(exist_ok=True, parents=True)
85103
download_url = f"https://crates.io/api/v1/crates/{dep_name}/{dep_version}/download"
86104
download_binary_file(download_url, download_path)
87105
must_match_any_checksum(download_path, [checksum_info])
106+
vendored_dep = prepare_crate_as_vendored_dep(download_path)
88107
downloads.append(
89108
{
90109
"package": dep_name,
91110
"name": dep_name,
92111
"version": dep_version,
93-
"path": download_path,
112+
"path": vendored_dep,
94113
"type": "cargo",
95114
"dev": False,
96115
}
97116
)
98117
return downloads
118+
119+
def _calc_sha256(content: bytes):
120+
return hashlib.sha256(content).hexdigest()
121+
122+
def generate_cargo_checksum(crate_path: Path):
123+
"""Generate Cargo checksums
124+
125+
cargo requires vendored dependencies to have a ".cargo_checksum.json" BUT crates
126+
downloaded from crates.io don't come with this file. This function generates
127+
a dictionary compatible what cargo expects.
128+
129+
Args:
130+
crate_path (Path): crate tarball
131+
132+
Returns:
133+
dict: checksums expected by cargo
134+
"""
135+
checksums = {"package": _calc_sha256(crate_path.read_bytes()), "files": {}}
136+
tarball = tarfile.open(crate_path)
137+
for tarmember in tarball.getmembers():
138+
name = tarmember.name.split("/", 1)[1] # ignore folder name
139+
checksums["files"][name] = _calc_sha256(tarball.extractfile(tarmember.name).read())
140+
tarball.close()
141+
return checksums
142+
143+
144+
def prepare_crate_as_vendored_dep(crate_path: Path) -> Path:
145+
"""Prepare crates as vendored dependencies
146+
147+
Extracts contents from crate and add a ".cargo_checksum.json" file to it
148+
149+
Args:
150+
crate_path (Path): crate tarball
151+
"""
152+
checksums = generate_cargo_checksum(crate_path)
153+
with tarfile.open(crate_path) as tarball:
154+
folder_name = tarball.getnames()[0].split("/")[0]
155+
tarball.extractall(crate_path.parent)
156+
cargo_checksum = crate_path.parent / folder_name / ".cargo-checksum.json"
157+
json.dump(checksums, cargo_checksum.open("w"))
158+
return crate_path.parent / folder_name

docs/usage.md

Lines changed: 82 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -504,97 +504,133 @@ podman build . \
504504
### Prefetch dependencies (cargo)
505505

506506
```shell
507-
mkdir -p playground/pure-rust
508-
cd playground/pure-rust
507+
mkdir -p /tmp/playground/pure-rust
508+
cd /tmp/playground/pure-rust
509509
git clone [email protected]:sharkdp/bat.git --branch=v0.22.1
510510
cachi2 fetch-deps --source bat '{"type": "cargo"}'
511511
```
512512

513513
### Generate environment variables (cargo)
514514

515-
#### Alternative crates.io
516-
Cargo must be configured differently when crates.io will be replaced with an alternative registry to crates.io (or at least similar way - something that the [cargo plugin for nexus](https://github.com/sonatype-nexus-community/nexus-repository-cargo) seems to handle) or from a local path.
515+
At the moment no env var is generated for cargo, but let's do this step for compatibility
516+
with other integrations.
517517

518-
For enabling an alternative crates.io registry the only required env is
519-
520-
```
521-
CARGO_REGISTRIES_MY_REGISTRY_INDEX=https://my-intranet:8080/git/index
518+
```shell
519+
cachi2 generate-env ./cachi2-output -o ./cachi2.env --for-output-dir /tmp/cachi2-output
522520
```
523521

524-
[cargo docs on using an alternate registry](https://doc.rust-lang.org/cargo/reference/registries.html#using-an-alternate-registry)
522+
### Inject project files (cargo)
523+
524+
```shell
525+
$ cachi2 inject-files $(realpath cachi2-output) --for-output-dir /tmp/cachi2-output
526+
2023-10-18 14:51:01,936 INFO Creating /tmp/playground/pure-rust/bat/.cargo/config.toml
527+
```
525528

526-
Unfortunately I'm still clueless on how to run an alternative registry (and this don't seem to align with cachi2 ethos), so I'm skipping this option.
529+
### Build the base image (cargo)
527530

528-
#### Replacing crates.io with a local path
529531

530-
TLDR: not possible through environment variables; will need to inject a config file to a specific location
532+
Containerfile.baseimage
533+
```Dockerfile
534+
FROM registry.access.redhat.com/ubi9/ubi
531535

532-
Unfortunately this method does not work with environment variables (see [this cargo issue](https://github.com/rust-lang/cargo/issues/5416)) and requires a .cargo/config.toml file.
536+
RUN dnf install cargo rust rust-std-static -y &&\
537+
dnf clean all
538+
```
533539

534-
It must be placed somewhere relative to where "cargo install/build" will run following the hierarchical structure below
540+
```shell
541+
podman build --tag bat-base-image -f Containerfile.baseimage .
542+
```
535543

536-
>Cargo allows local configuration for a particular package as well as global configuration. It looks for configuration files in the current directory and all parent directories. If, for example, Cargo were invoked in `/projects/foo/bar/baz`, then the following configuration files would be probed for and unified in this order:
537-
>- `/projects/foo/bar/baz/.cargo/config.toml`
538-
>- `/projects/foo/bar/.cargo/config.toml`
539-
>- `/projects/foo/.cargo/config.toml`
540-
>- `/projects/.cargo/config.toml`
541-
>- `/.cargo/config.toml`
542-
>- `$CARGO_HOME/config.toml` which defaults to:
543-
> - Windows: `%USERPROFILE%\.cargo\config.toml`
544-
> - Unix: `$HOME/.cargo/config.toml`
544+
### Build the application image (cargo)
545545

546-
Source: [cargo docs](https://doc.rust-lang.org/cargo/reference/config.html#hierarchical-structure)
546+
Containerfile
547+
```Dockerfile
548+
FROM bat-base-image:latest
547549

548-
This is how I configured mine:
550+
COPY bat /app
551+
WORKDIR /app
552+
RUN source /tmp/cachi2.env && \
553+
cargo install --locked --path .
554+
ENV PATH="/root/.cargo/bin:$PATH"
555+
CMD bat
556+
```
549557

550-
cargo_config.toml
558+
```shell
559+
podman build . \
560+
--volume "$(realpath ./cachi2-output)":/tmp/cachi2-output:Z \
561+
--volume "$(realpath ./cachi2.env)":/tmp/cachi2.env:Z \
562+
--network none \
563+
--tag bat
551564
```
552-
[source.crates-io]
553-
replace-with = "local"
554565

555-
[source.local]
556-
local-registry = "/tmp/cachi2-output"
566+
## Example: pip with indirect cargo dependencies
567+
568+
### Prefetch dependencies (pip + cargo)
569+
570+
```shell
571+
mkdir -p /tmp/playground/python-cargo
572+
cd /tmp/playground/python-cargo
573+
git clone [email protected]:/bruno-fs/simple-python-rust-project --branch=0.0.1 dummy
574+
cachi2 fetch-deps --source dummy '[{"type": "pip"}, {"type": "cargo", "lock_file": "merged-cargo.lock", "pkg_name": "dummy", "pkg_version": "0.0.1"}]'
557575
```
558-
local-registry also requires a index of all packages downloaded exactly like what's in https://github.com/rust-lang/crates.io-index. For this POC I just git cloned this repo locally and placed it at `./cachi2-output/index` (NOTE: this is definitely overkill; the final version should use only indexes for the packages we have)
576+
577+
### Generate environment variables (pip + cargo)
559578

560579
```shell
561-
git clone [email protected]:rust-lang/crates.io-index.git --depth 1 cachi2-output/index
580+
cachi2 generate-env ./cachi2-output -o ./cachi2.env --for-output-dir /tmp/cachi2-output
562581
```
563582

564-
There's a [cargo local registry package](https://crates.io/crates/cargo-local-registry) that prepares dependencies like this, but it is pretty buggy and fails for some packages I played around (including the one used on this PoC).
583+
### Inject project files (pip + cargo)
565584

566-
### Build the base image (cargo)
585+
```shell
586+
$ cachi2 inject-files $(realpath cachi2-output) --for-output-dir /tmp/cachi2-output
587+
2023-10-18 14:51:01,936 INFO Creating /tmp/playground/python-cargo/dummy/.cargo/config.toml
588+
```
589+
590+
### Build the base image (pip + cargo)
567591

568592

569593
Containerfile.baseimage
570594
```Dockerfile
571-
FROM registry.redhat.io/ubi9/ubi
595+
FROM quay.io/centos/centos:stream8
572596

573-
RUN dnf install cargo rust rust-std-static -y &&\
597+
RUN dnf -y install \
598+
python3.11 \
599+
python3.11-pip \
600+
python3.11-devel \
601+
gcc \
602+
libffi-devel \
603+
openssl-devel \
604+
cargo \
605+
rust \
606+
rust-std-static &&\
574607
dnf clean all
575608
```
576609

577610
```shell
578-
podman build --tag bat-base-image -f Containerfile.baseimage .
611+
podman build --tag dummy-base-image -f Containerfile.baseimage .
579612
```
580613

581-
### Build the application image (cargo)
614+
### Build the application image (pip + cargo)
582615

583616
Containerfile
584617
```Dockerfile
585-
FROM bat-base-image:latest
618+
FROM dummy-base-image:latest
586619

587-
COPY cargo_config.toml /app/.cargo/config.toml
588-
COPY bat /app
620+
COPY dummy /app
621+
# we don't have a way to control where pip will build
622+
# cargo dependencies, so we need to move cargo configuration
623+
# to the place where python run builds
624+
COPY dummy/.cargo/config.toml /tmp/.cargo/config.toml
589625
WORKDIR /app
590-
RUN cargo install --locked --path .
591-
ENV PATH="/root/.cargo/bin:$PATH"
592-
CMD bat
626+
RUN source /tmp/cachi2.env && \
627+
pip3 install -r requirements.txt
593628
```
594629

595630
```shell
596631
podman build . \
597632
--volume "$(realpath ./cachi2-output)":/tmp/cachi2-output:Z \
633+
--volume "$(realpath ./cachi2.env)":/tmp/cachi2.env:Z \
598634
--network none \
599-
--tag bat
600-
```
635+
--tag dummy
636+
```

0 commit comments

Comments
 (0)