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
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ clap = { version = "4.6", features = ["derive"] }
console = "0.16"
dirs = "6"
futures = "0.3"
fs4 = "0.13"
indicatif = "0.18"
miette = { version = "7.6", features = ["fancy"] }
rattler = { version = "0.46", default-features = false, features = ["indicatif"] }
Expand Down
12 changes: 12 additions & 0 deletions docs/explanation/install-locations-and-ownership.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ managed prefix.
It records:

- schema version
- bootstrap state
- display name
- install name
- metadata filename
Expand All @@ -68,6 +69,17 @@ It records:
- package names

Later runtime invocations check that metadata before reusing a prefix.
The metadata file marks bootstrap complete. Metadata written by older
conda-ship runtimes is accepted when its ownership identity and delegate still
validate.

While bootstrap is running, the runtime holds a lock in the prefix's parent
directory and writes a separate internal `installing` marker inside the prefix.
The marker identifies the runtime that started bootstrap. A later invocation
waits for a live bootstrap to release the lock, then checks the prefix again. If
the previous process stopped and the marker matches this runtime, recovery
reinstalls every locked package and reruns post-link scripts without deleting
the prefix.

This ownership file is conda-ship-specific. The runtime also writes standard
conda prefix metadata:
Expand Down
3 changes: 2 additions & 1 deletion docs/reference/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -239,7 +239,8 @@ runtime name as uppercased `RUNTIME_NAME` plus `_PREFIX`.

At bootstrap time, the generated runtime writes a separate prefix metadata file
inside the managed prefix. That file is used for ownership checks before later
operations touch the prefix.
operations touch the prefix. It is written last to mark bootstrap complete.
The internal installing marker is then removed.

The bootstrap also writes standard conda prefix metadata:

Expand Down
4 changes: 4 additions & 0 deletions docs/reference/errors.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,3 +100,7 @@ conda without depending on terminal formatting.

`refusing to use unmanaged install path`
: The prefix does not contain ownership metadata for this runtime.

`refusing to use install path with invalid bootstrap state`
: The internal installing marker is malformed or belongs to another runtime.
A non-empty prefix without matching ownership state is rejected.
12 changes: 12 additions & 0 deletions docs/reference/runtime-cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,18 @@ prefix metadata expected by conda tools in `conda-meta/history` and
build configured `condarc-file`, and writes the CEP 22 frozen marker only when
the build configured `freeze-base = true`.

Bootstrap is serialized with a process lock next to the managed prefix. An
internal `installing` marker identifies an incomplete prefix owned by this
runtime. The runtime metadata file is written after package installation,
post-link scripts, prefix metadata, configured policy, bytecode compilation,
and delegate validation finish. Its `ready` state marks bootstrap complete.

If bootstrap is interrupted, the next invocation automatically retries only
when that internal marker belongs to the same stamped runtime. Recovery forces
every locked package through Rattler's reinstall path so post-link scripts run
again. It does not delete the prefix, named environments, or unrelated paths.
An unknown non-empty prefix is still refused.

## Delegate Execution

After the prefix is available, every argument belongs to the delegate. The
Expand Down
84 changes: 84 additions & 0 deletions src/bootstrap_lock.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
//! Cross-process serialization for automatic runtime bootstrap.

use std::fs::{File, OpenOptions};
use std::path::{Path, PathBuf};

use fs4::fs_std::FileExt;
use miette::{Context, IntoDiagnostic};

use crate::policy;

pub(crate) struct BootstrapLock {
_file: File,
}

impl BootstrapLock {
pub(crate) fn acquire(prefix: &Path) -> miette::Result<Self> {
let path = path(prefix)?;
let parent = path
.parent()
.ok_or_else(|| miette::miette!("bootstrap lock has no parent directory"))?;
std::fs::create_dir_all(parent)
.into_diagnostic()
.with_context(|| {
format!(
"failed to create bootstrap lock directory at {}",
policy::path_for_display(parent)
)
})?;

let file = OpenOptions::new()
.read(true)
.write(true)
.create(true)
.truncate(false)
.open(&path)
.into_diagnostic()
.with_context(|| {
format!(
"failed to open bootstrap lock at {}",
policy::path_for_display(&path)
)
})?;
file.lock_exclusive().into_diagnostic().with_context(|| {
format!(
"failed to acquire bootstrap lock at {}",
policy::path_for_display(&path)
)
})?;
Ok(Self { _file: file })
}
}

pub(crate) fn path(prefix: &Path) -> miette::Result<PathBuf> {
let parent = prefix.parent().ok_or_else(|| {
miette::miette!(
"install path has no parent directory: {}",
policy::path_for_display(prefix)
)
})?;
let name = prefix.file_name().ok_or_else(|| {
miette::miette!(
"install path has no final component: {}",
policy::path_for_display(prefix)
)
})?;
Ok(parent.join(format!(".{}.conda-ship.lock", name.to_string_lossy())))
}

#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;

#[test]
fn test_lock_is_adjacent_to_prefix() {
let tmp = TempDir::new().unwrap();
let prefix = tmp.path().join("runtime");

let lock = path(&prefix).unwrap();

assert_eq!(lock.parent(), prefix.parent());
assert!(!lock.starts_with(&prefix));
}
}
Loading
Loading