Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,11 @@ Include `conda-self` when a conda runtime should
expose `conda self reset` for restoring the bootstrapped base prefix to the
initial package set shipped by the runtime.

conda-ship does not choose conda configuration. Set `condarc-file` to copy a
YAML condarc file to `<prefix>/.condarc`, and set
`freeze-base = true` when it should write a CEP 22 frozen marker. Both are
disabled by default.

## Local Workflow

Packaged builds find `cs-template` next to the installed `cs` executable.
Expand Down
1 change: 1 addition & 0 deletions docs/explanation/project-boundaries.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ Downstream projects decide what their users get:
- package sets
- channels
- package exclusions
- installed condarc contents and frozen-base policy
- default release channels
- documentation URLs
- Homebrew formulae
Expand Down
6 changes: 4 additions & 2 deletions docs/explanation/runtime-template.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ binary:
- documentation URL
- metadata filename
- bundle, offline, and prefix environment variable names
- optional condarc contents and base-freezing setting

That is what turns the same generic bootstrap code into a specific runtime
with its own runtime name, delegate, package set, and install location.
Expand Down Expand Up @@ -67,8 +68,9 @@ the alias from
[conda-spawn PR #59](https://github.com/conda/conda-spawn/pull/59) can expose
`RUNTIME shell` as a command provided by conda-spawn.

The base prefix is protected with a CEP 22 frozen marker. Users create named
environments for regular package work.
Downstream distributions can stamp native condarc contents and protect the base
prefix with a CEP 22 frozen marker. Without those opt-ins, conda-ship leaves
conda configuration and package-created frozen markers untouched.

## What Each Project Chooses

Expand Down
27 changes: 27 additions & 0 deletions docs/how-to/customize-runtime.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,33 @@ conda-self = "*"
conda-ship writes `conda-meta/initial-state.explicit.txt` during bootstrap.
`conda-self` treats that file as the installer snapshot for reset commands.

## Configure Condarc And Base Freezing

By default, conda-ship does not create `.condarc` or freeze the managed base
prefix. A downstream conda distribution can opt into both behaviors:

```toml
[tool.conda-ship]
condarc-file = "runtime.condarc"
freeze-base = true
```

Keep `runtime.condarc` as YAML next to the selected manifest:

```yaml
channels:
- conda-forge
solver: rattler
auto_activate_base: false
notify_outdated_conda: false
show_channel_urls: true
```

The builder validates that the file contains a YAML mapping and stamps its
exact text. It does not derive or merge lockfile channels into this file.
Omitting `condarc-file` leaves `.condarc` alone. Leaving `freeze-base` false
also preserves any frozen marker created by an installed package.

## Configure Build Input

Keep package and channel intent in the manifest format owned by your workspace
Expand Down
6 changes: 3 additions & 3 deletions docs/how-to/package-a-runtime.md
Original file line number Diff line number Diff line change
Expand Up @@ -128,9 +128,9 @@ demo info
```

The installer should not unpack the managed conda prefix by itself. Let the
runtime bootstrap so ownership metadata, `.condarc`, the frozen marker,
constructor-compatible prefix metadata, and package verification are applied
consistently.
runtime bootstrap so ownership metadata, configured condarc and frozen-base
policy, constructor-compatible prefix metadata, and package verification are
applied consistently.

```{warning}
Do not preinstall the managed prefix behind the runtime's back. Runtime
Expand Down
29 changes: 26 additions & 3 deletions docs/reference/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,8 @@ docs-url = "https://example.com/demo/"
install-scheme = "conda-home"
install-name = "demo"
installer = "homebrew"
condarc-file = "runtime.condarc"
freeze-base = true
```

For the naming model behind `runtime-name`, `artifact-name`, `install-name`, and
Expand Down Expand Up @@ -174,6 +176,21 @@ For the naming model behind `runtime-name`, `artifact-name`, `install-name`, and
Release workflows can override this with `cs build --installer INSTALLER` or
the GitHub Action `installer` input.

`condarc-file`
: Optional path to a YAML condarc file. Relative paths are resolved from
the selected project manifest. The builder requires a YAML mapping and stamps
the file's exact text content into the runtime. During bootstrap, the runtime
writes that content to `<prefix>/.condarc`.

When omitted, conda-ship does not create, replace, or remove `.condarc`.
The runtime lock still records the channels used to build the prefix.
conda-ship does not merge them into persistent conda configuration.

`freeze-base`
: Whether bootstrap writes the existing CEP 22 marker to
`<prefix>/conda-meta/frozen`. Defaults to `false`. When false, conda-ship
leaves any marker created by an installed package untouched.

Generated runtimes write ownership metadata into every bootstrapped prefix.
That metadata records the schema version, display name derived from
`runtime-name`, install name, and metadata filename expected by the runtime.
Expand All @@ -190,9 +207,10 @@ stamped runtime lock. When `conda-self` is installed in the runtime, it uses
that file as the installer snapshot for the `installer-updated` and
`installer-exact` reset modes.

Package and channel intent belongs in the selected source environment, not in
`[tool.conda-ship]`. conda-ship records the resolved package names and channel
URLs from the source lockfile environment into generated runtime metadata.
Keep package selection and lockfile channels in the selected source environment.
conda-ship records the resolved package names and channel URLs in runtime
metadata. It writes persistent conda configuration only when `condarc-file` is
set.

## Stamped Runtime Metadata

Expand All @@ -210,6 +228,8 @@ URLs from the source lockfile environment into generated runtime metadata.
- install scheme: `conda-home`, or the configured `install-scheme`
- install name: `RUNTIME_NAME`, or the configured `install-name`
- installer: the configured `installer`, when present
- condarc contents: the exact text from `condarc-file`, when configured
- frozen base policy: the configured `freeze-base` value, defaulting to `false`
- metadata file: `.RUNTIME_NAME.json`
- bundle environment variable: uppercased `RUNTIME_NAME` plus `_BUNDLE`
- offline environment variable: uppercased `RUNTIME_NAME` plus `_OFFLINE`
Expand All @@ -229,6 +249,9 @@ The bootstrap also writes standard conda prefix metadata:
These files are not stamped into the runtime binary. They are rendered from the
runtime lock when the prefix is bootstrapped.

The runtime writes `.condarc` and the CEP 22 frozen marker only when their
corresponding options are set.

Non-alphanumeric characters in environment variable names become underscores.

## Downstream Defaults
Expand Down
11 changes: 11 additions & 0 deletions docs/reference/errors.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,17 @@ conda without depending on terminal formatting.
`runtime-version = { from = "project-metadata" }` through the Python
`conda ship` adapter for dynamic Python project versions.

`failed to read condarc-file`
: Check `[tool.conda-ship].condarc-file`. Relative paths start from the selected
manifest directory.

`failed to parse condarc-file as YAML`
: Fix the referenced YAML file.

`condarc-file must contain a YAML mapping`
: Use normal condarc key and value entries rather than a top-level scalar or
sequence.

## Package Archives

`cannot bundle packages without SHA256 hashes`
Expand Down
8 changes: 5 additions & 3 deletions docs/reference/runtime-cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,11 @@ Online artifacts download packages from the stamped runtime lock. External
artifacts read archives from the configured bundle directory. Embedded
artifacts automatically extract their built-in bundle.

During bootstrap, the runtime writes conda-ship ownership metadata,
`.condarc`, the CEP 22 frozen marker, and the prefix metadata expected by conda
tools in `conda-meta/history` and `conda-meta/initial-state.explicit.txt`.
During bootstrap, the runtime writes conda-ship ownership metadata and the
prefix metadata expected by conda tools in `conda-meta/history` and
`conda-meta/initial-state.explicit.txt`. It writes `.condarc` only when the
build configured `condarc-file`, and writes the CEP 22 frozen marker only when
the build configured `freeze-base = true`.

## Delegate Execution

Expand Down
3 changes: 2 additions & 1 deletion docs/reference/runtime-data-format.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,8 @@ The stamped header records:
: Optional package manager or installer metadata.

`runtime_config`
: Resolved runtime channels and package names used for bootstrap metadata.
: Resolved runtime channels and package names used for bootstrap metadata, plus
optional stamped condarc text and the frozen-base policy.

`runtime_lock`
: Runtime lock used for bootstrap.
Expand Down
6 changes: 6 additions & 0 deletions src/bin/cs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,10 @@ struct ShipConfig {
install_name: Option<String>,
#[serde(default)]
installer: Option<String>,
#[serde(default, rename = "condarc-file")]
condarc_file: Option<PathBuf>,
#[serde(default, rename = "freeze-base")]
freeze_base: bool,
}

#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
Expand Down Expand Up @@ -108,6 +112,8 @@ struct RuntimeStampConfig {
install_scheme: Option<runtime_data::InstallScheme>,
install_name: Option<String>,
installer: Option<String>,
condarc: Option<String>,
freeze_base: bool,
}

#[derive(Parser)]
Expand Down
2 changes: 2 additions & 0 deletions src/bin/cs/artifact.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1381,6 +1381,8 @@ fn stamp_runtime_data(
runtime_config: runtime_data::RuntimeConfig {
channels: derived.runtime_config.channels.clone(),
packages: derived.runtime_config.packages.clone(),
condarc: derived.runtime_config.condarc.clone(),
freeze_base: derived.runtime_config.freeze_base,
},
runtime_lock: derived.content.clone(),
};
Expand Down
29 changes: 29 additions & 0 deletions src/bin/cs/project.rs
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ impl ManifestKind {

pub(crate) fn derive_runtime_lock(root: &Path) -> miette::Result<DerivedRuntimeLock> {
let input = discover_project_input(root)?;
let condarc = read_condarc_file(&input.manifest_path, input.config.condarc_file.as_deref())?;
let lock_content = std::fs::read_to_string(&input.lock_path)
.into_diagnostic()
.with_context(|| format!("failed to read {}", input.lock_path.display()))?;
Expand Down Expand Up @@ -194,6 +195,8 @@ pub(crate) fn derive_runtime_lock(root: &Path) -> miette::Result<DerivedRuntimeL
install_scheme: input.config.install_scheme,
install_name: input.config.install_name,
installer: input.config.installer,
condarc,
freeze_base: input.config.freeze_base,
},
platforms,
total_packages,
Expand All @@ -202,6 +205,32 @@ pub(crate) fn derive_runtime_lock(root: &Path) -> miette::Result<DerivedRuntimeL
})
}

pub(crate) fn read_condarc_file(
manifest_path: &Path,
configured_path: Option<&Path>,
) -> miette::Result<Option<String>> {
let Some(configured_path) = configured_path else {
return Ok(None);
};
let path = manifest_path
.parent()
.unwrap_or_else(|| Path::new(""))
.join(configured_path);
let contents = std::fs::read_to_string(&path)
.into_diagnostic()
.with_context(|| format!("failed to read condarc-file {}", path.display()))?;
let document: serde_yaml::Value = serde_yaml::from_str(&contents)
.into_diagnostic()
.with_context(|| format!("failed to parse condarc-file {} as YAML", path.display()))?;
if !document.is_mapping() {
return Err(miette::miette!(
"condarc-file must contain a YAML mapping: {}",
path.display()
));
}
Ok(Some(contents))
}

pub(crate) fn discover_project_input(root: &Path) -> miette::Result<ProjectInput> {
let manifest_path = discover_manifest_path(root)?;
let kind = manifest_kind(&manifest_path)?;
Expand Down
49 changes: 48 additions & 1 deletion src/bin/cs/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ use super::diagnostic::{DiagnosticKind, ShipDiagnostic};
use super::project::{
DerivedRuntimeLock, ManifestKind, ProjectInput, derive_runtime_lock, discover_manifest_path,
discover_project_input, filter_excluded, find_project_root, is_supported_pyproject_manifest,
manifest_kind,
manifest_kind, read_condarc_file,
};
use super::{
BundleLayout, Cli, Command, RUNTIME_TEMPLATE_ENV, RuntimeStampConfig, RuntimeVersionConfig,
Expand Down Expand Up @@ -285,6 +285,8 @@ delegate-executable = "conda"
artifact-layout = "embedded"
exclude-packages = ["conda-libmamba-solver"]
installer = "homebrew"
condarc-file = "runtime.condarc"
freeze-base = true
"#,
)
.unwrap();
Expand All @@ -305,6 +307,44 @@ installer = "homebrew"
manifest.tool.conda_ship.installer.as_deref(),
Some("homebrew")
);
assert_eq!(
manifest.tool.conda_ship.condarc_file.as_deref(),
Some(Path::new("runtime.condarc"))
);
assert!(manifest.tool.conda_ship.freeze_base);
}

#[test]
fn test_read_condarc_file_resolves_from_manifest_and_preserves_text() {
let tmp = TempDir::new().unwrap();
let project = tmp.path().join("project");
std::fs::create_dir(&project).unwrap();
let manifest = project.join("conda.toml");
std::fs::write(&manifest, "").unwrap();
let expected = "# downstream policy\nchannels:\n - conda-forge\n";
std::fs::write(project.join("runtime.condarc"), expected).unwrap();

let contents = read_condarc_file(&manifest, Some(Path::new("runtime.condarc")))
.unwrap()
.unwrap();

assert_eq!(contents, expected);
}

#[rstest]
#[case::sequence("- conda-forge\n", "must contain a YAML mapping")]
#[case::invalid("channels: [\n", "failed to parse condarc-file")]
fn test_read_condarc_file_rejects_invalid_input(#[case] contents: &str, #[case] expected: &str) {
let tmp = TempDir::new().unwrap();
let manifest = tmp.path().join("conda.toml");
std::fs::write(&manifest, "").unwrap();
std::fs::write(tmp.path().join("runtime.condarc"), contents).unwrap();

let error = read_condarc_file(&manifest, Some(Path::new("runtime.condarc")))
.expect_err("invalid condarc should fail")
.to_string();

assert!(error.contains(expected), "{error}");
}

#[rstest]
Expand Down Expand Up @@ -567,6 +607,8 @@ fn test_stage_artifacts_external_uses_artifact_name_for_files() {
delegate_executable: Some("conda".to_string()),
runtime_version: Some("9.8.7".to_string()),
installer: Some("homebrew".to_string()),
condarc: Some("solver: rattler\n".to_string()),
freeze_base: true,
..RuntimeStampConfig::default()
},
platforms: vec![platform],
Expand Down Expand Up @@ -607,6 +649,11 @@ fn test_stage_artifacts_external_uses_artifact_name_for_files() {
assert_eq!(stamped.header.bundle_env_var, "DEMO_BUNDLE");
assert_eq!(stamped.header.runtime_version, "9.8.7");
assert_eq!(stamped.header.installer.as_deref(), Some("homebrew"));
assert_eq!(
stamped.header.runtime_config.condarc.as_deref(),
Some("solver: rattler\n")
);
assert!(stamped.header.runtime_config.freeze_base);
assert_eq!(
stamped.header.runtime_config.packages,
vec!["conda".to_string()]
Expand Down
Loading
Loading