pub struct InstallPlan { /* private fields */ }Expand description
An explicit installation root and a coherent artifact set ready for private preparation.
Implementations§
Source§impl InstallPlan
impl InstallPlan
Sourcepub fn new(
product: ProductId,
release: ReleaseId,
installation_root: impl Into<PathBuf>,
artifacts: ArtifactSet,
) -> Result<Self>
pub fn new( product: ProductId, release: ReleaseId, installation_root: impl Into<PathBuf>, artifacts: ArtifactSet, ) -> Result<Self>
Validates an installation root, destination containment, sources, and member uniqueness.
Examples found in repository?
35fn build(
36 new: &[u8],
37 old: Option<&[u8]>,
38) -> Result<(PathBuf, InstallPlan), Box<dyn std::error::Error>> {
39 let base = temp_root("eggup-receipts");
40 let inputs = base.join("inputs");
41 let root = base.join("install");
42 fs::create_dir_all(&inputs)?;
43 fs::create_dir_all(root.join("bin"))?;
44 fs::write(inputs.join("app"), new)?;
45 if let Some(old) = old {
46 fs::write(root.join("bin/app"), old)?;
47 }
48 let digest = eggup_core::hash_file(&inputs.join("app"))?;
49 let member = ArtifactMember::new(MemberId::new("main")?, inputs.join("app"), "bin/app")?
50 .with_integrity(IntegrityRequirement::Sha256(digest));
51 let plan = InstallPlan::new(
52 ProductId::new("demo")?,
53 ReleaseId::new("5.0.0")?,
54 &root,
55 ArtifactSet::single(member)?,
56 )?;
57 // Leak the base via the plan's root; caller cleans `root.parent()`.
58 Ok((base, plan))
59}More examples
46fn main() -> Result<(), Box<dyn std::error::Error>> {
47 let base = temp_root("eggup-custom-validator");
48 let inputs = base.join("inputs");
49 let root = base.join("install");
50 fs::create_dir_all(&inputs)?;
51 fs::create_dir_all(root.join("bin"))?;
52 fs::write(inputs.join("app"), b"release-marker-42")?;
53 let digest = eggup_core::hash_file(&inputs.join("app"))?;
54 let member = ArtifactMember::new(MemberId::new("main")?, inputs.join("app"), "bin/app")?
55 .with_integrity(IntegrityRequirement::Sha256(digest));
56 let plan = InstallPlan::new(
57 ProductId::new("demo")?,
58 ReleaseId::new("3.0.0")?,
59 &root,
60 ArtifactSet::single(member)?,
61 )?;
62 let validator = ContainsMarker {
63 member: MemberId::new("main")?,
64 marker: b"marker-42".to_vec(),
65 };
66 let receipt = plan
67 .prepare()?
68 .verify_integrity()?
69 .validate(&validator)?
70 .commit(CommitOwnership::new(
71 &ExistingAsOwnedVerifier,
72 AbsentPolicy::AllowCreate,
73 ))?;
74 assert_eq!(receipt.disposition(), TransactionDisposition::Committed);
75 let _ = fs::remove_dir_all(&base);
76 println!("custom validator commit: {:?}", receipt.disposition());
77 Ok(())
78}30fn main() -> Result<(), Box<dyn std::error::Error>> {
31 let base = temp_root("eggup-quickstart");
32 let (inputs, root) = (base.join("inputs"), base.join("install"));
33 fs::create_dir_all(inputs.join("bin"))?;
34 fs::create_dir_all(root.join("bin"))?;
35 fs::write(inputs.join("app"), b"new-app")?;
36
37 // 1. Describe one member, with the SHA-256 you expect to verify.
38 let source = inputs.join("app");
39 let digest = eggup_core::hash_file(&source)?;
40 let member = ArtifactMember::new(MemberId::new("main")?, source, "bin/app")?
41 .with_integrity(IntegrityRequirement::Sha256(digest));
42
43 // 2. Name the product, release, install root, and artifact set.
44 let plan = InstallPlan::new(
45 ProductId::new("demo")?,
46 ReleaseId::new("1.0.0")?,
47 &root,
48 ArtifactSet::single(member)?,
49 )?;
50
51 // 3. Stage, verify, validate, then commit with proven ownership.
52 let receipt = plan
53 .prepare()?
54 .verify_integrity()?
55 .validate(&AllValidators::new())?
56 .commit(CommitOwnership::new(
57 &ExistingAsOwnedVerifier,
58 AbsentPolicy::AllowCreate,
59 ))?;
60
61 assert_eq!(receipt.disposition(), TransactionDisposition::Committed);
62 assert_eq!(fs::read(root.join("bin/app"))?, b"new-app");
63 let _ = fs::remove_dir_all(&base);
64 println!("committed: {:?}", receipt.disposition());
65 Ok(())
66}24fn main() -> Result<(), Box<dyn std::error::Error>> {
25 let base = temp_root("eggup-ownership");
26 let inputs = base.join("inputs");
27 let root = base.join("install");
28 fs::create_dir_all(&inputs)?;
29 fs::create_dir_all(root.join("bin"))?;
30 fs::write(inputs.join("app"), b"new-app")?;
31 fs::write(root.join("bin/app"), b"old-app")?;
32 let new_digest = eggup_core::hash_file(&inputs.join("app"))?;
33 let old_digest = eggup_core::hash_file(&root.join("bin/app"))?;
34 let member = ArtifactMember::new(MemberId::new("main")?, inputs.join("app"), "bin/app")?
35 .with_integrity(IntegrityRequirement::Sha256(new_digest));
36 let plan = InstallPlan::new(
37 ProductId::new("demo")?,
38 ReleaseId::new("4.0.0")?,
39 &root,
40 ArtifactSet::single(member)?,
41 )?;
42 // Prove the live file is the expected old deployment; deny creation of
43 // absent destinations (replacement-only policy).
44 let verifier = ExactDigestVerifier::new(vec![(MemberId::new("main")?, old_digest)]);
45 let receipt = plan
46 .prepare()?
47 .verify_integrity()?
48 .validate(&AllValidators::new())?
49 .commit(CommitOwnership::new(&verifier, AbsentPolicy::DenyCreate))?;
50 assert_eq!(receipt.disposition(), TransactionDisposition::Committed);
51 assert_eq!(fs::read(root.join("bin/app"))?, b"new-app");
52 let _ = fs::remove_dir_all(&base);
53 println!("ownership-verified commit: {:?}", receipt.disposition());
54 Ok(())
55}24fn main() -> Result<(), Box<dyn std::error::Error>> {
25 let base = temp_root("eggup-multi-member");
26 let inputs = base.join("inputs");
27 let root = base.join("install");
28 fs::create_dir_all(&inputs)?;
29 fs::create_dir_all(root.join("bin"))?;
30 fs::write(inputs.join("main"), b"new-main")?;
31 fs::write(inputs.join("helper"), b"new-helper")?;
32 fs::write(root.join("bin/main"), b"old-main")?;
33 fs::write(root.join("bin/helper"), b"old-helper")?;
34 let main_digest = eggup_core::hash_file(&inputs.join("main"))?;
35 let helper_digest = eggup_core::hash_file(&inputs.join("helper"))?;
36 let members = ArtifactSet::new(vec![
37 ArtifactMember::new(MemberId::new("main")?, inputs.join("main"), "bin/main")?
38 .with_integrity(IntegrityRequirement::Sha256(main_digest)),
39 ArtifactMember::new(
40 MemberId::new("helper")?,
41 inputs.join("helper"),
42 "bin/helper",
43 )?
44 .with_integrity(IntegrityRequirement::Sha256(helper_digest)),
45 ])?;
46 let plan = InstallPlan::new(
47 ProductId::new("bundle")?,
48 ReleaseId::new("2.0.0")?,
49 &root,
50 members,
51 )?;
52 let receipt = plan
53 .prepare()?
54 .verify_integrity()?
55 .validate(&AllValidators::new())?
56 .commit(CommitOwnership::new(
57 &ExistingAsOwnedVerifier,
58 AbsentPolicy::AllowCreate,
59 ))?;
60 assert_eq!(receipt.disposition(), TransactionDisposition::Committed);
61 assert_eq!(fs::read(root.join("bin/main"))?, b"new-main");
62 assert_eq!(fs::read(root.join("bin/helper"))?, b"new-helper");
63 let _ = fs::remove_dir_all(&base);
64 println!("multi-member commit: {:?}", receipt.disposition());
65 Ok(())
66}Sourcepub fn for_current_executable(
product: ProductId,
release: ReleaseId,
candidate: impl Into<PathBuf>,
current: &CurrentExecutable,
integrity: IntegrityRequirement,
) -> Result<Self>
pub fn for_current_executable( product: ProductId, release: ReleaseId, candidate: impl Into<PathBuf>, current: &CurrentExecutable, integrity: IntegrityRequirement, ) -> Result<Self>
Builds a one-member plan that replaces the executable currently running.
This is an ordinary InstallPlan, not a second transaction model: it
advances through exactly the same prepared/verified/validated/receipt
path as any other one-member plan, and its terminal contract is the same
crate::TransactionReceipt. The only differences are authority and
identity:
- The installation root is the executable’s own directory, and staging is
StagePlacement::InsideInstallationRoot, so the transaction needs write authority in that directory and nowhere above it. - The single destination is the canonical live image, so an invocation symlink is followed to its real target instead of being overwritten.
- The bound executable identity is re-proved immediately before mutation.
The candidate must be a local path outside the executable’s directory,
exactly as for any other member. Nothing here selects a release, infers a
version, or reads product identity from the executable: product,
release, and integrity are all caller-supplied.
Sourcepub fn stage_placement(&self) -> StagePlacement
pub fn stage_placement(&self) -> StagePlacement
Returns where this plan creates its private staging state.
Sourcepub fn current_executable(&self) -> Option<&CurrentExecutable>
pub fn current_executable(&self) -> Option<&CurrentExecutable>
Returns the bound current executable when this plan replaces one.
Sourcepub fn installation_root(&self) -> &Path
pub fn installation_root(&self) -> &Path
Returns the exact installation root used for destination resolution.
Sourcepub fn artifacts(&self) -> &ArtifactSet
pub fn artifacts(&self) -> &ArtifactSet
Returns the validated artifact set.
Sourcepub fn destination(&self, member: &MemberId) -> Result<PathBuf>
pub fn destination(&self, member: &MemberId) -> Result<PathBuf>
Resolves a member destination beneath the exact installation root.
Sourcepub fn prepare(self) -> Result<PreparedTransaction>
pub fn prepare(self) -> Result<PreparedTransaction>
Copies every member into private owned stage state without mutating a destination.
Examples found in repository?
46fn main() -> Result<(), Box<dyn std::error::Error>> {
47 let base = temp_root("eggup-custom-validator");
48 let inputs = base.join("inputs");
49 let root = base.join("install");
50 fs::create_dir_all(&inputs)?;
51 fs::create_dir_all(root.join("bin"))?;
52 fs::write(inputs.join("app"), b"release-marker-42")?;
53 let digest = eggup_core::hash_file(&inputs.join("app"))?;
54 let member = ArtifactMember::new(MemberId::new("main")?, inputs.join("app"), "bin/app")?
55 .with_integrity(IntegrityRequirement::Sha256(digest));
56 let plan = InstallPlan::new(
57 ProductId::new("demo")?,
58 ReleaseId::new("3.0.0")?,
59 &root,
60 ArtifactSet::single(member)?,
61 )?;
62 let validator = ContainsMarker {
63 member: MemberId::new("main")?,
64 marker: b"marker-42".to_vec(),
65 };
66 let receipt = plan
67 .prepare()?
68 .verify_integrity()?
69 .validate(&validator)?
70 .commit(CommitOwnership::new(
71 &ExistingAsOwnedVerifier,
72 AbsentPolicy::AllowCreate,
73 ))?;
74 assert_eq!(receipt.disposition(), TransactionDisposition::Committed);
75 let _ = fs::remove_dir_all(&base);
76 println!("custom validator commit: {:?}", receipt.disposition());
77 Ok(())
78}More examples
30fn main() -> Result<(), Box<dyn std::error::Error>> {
31 let base = temp_root("eggup-quickstart");
32 let (inputs, root) = (base.join("inputs"), base.join("install"));
33 fs::create_dir_all(inputs.join("bin"))?;
34 fs::create_dir_all(root.join("bin"))?;
35 fs::write(inputs.join("app"), b"new-app")?;
36
37 // 1. Describe one member, with the SHA-256 you expect to verify.
38 let source = inputs.join("app");
39 let digest = eggup_core::hash_file(&source)?;
40 let member = ArtifactMember::new(MemberId::new("main")?, source, "bin/app")?
41 .with_integrity(IntegrityRequirement::Sha256(digest));
42
43 // 2. Name the product, release, install root, and artifact set.
44 let plan = InstallPlan::new(
45 ProductId::new("demo")?,
46 ReleaseId::new("1.0.0")?,
47 &root,
48 ArtifactSet::single(member)?,
49 )?;
50
51 // 3. Stage, verify, validate, then commit with proven ownership.
52 let receipt = plan
53 .prepare()?
54 .verify_integrity()?
55 .validate(&AllValidators::new())?
56 .commit(CommitOwnership::new(
57 &ExistingAsOwnedVerifier,
58 AbsentPolicy::AllowCreate,
59 ))?;
60
61 assert_eq!(receipt.disposition(), TransactionDisposition::Committed);
62 assert_eq!(fs::read(root.join("bin/app"))?, b"new-app");
63 let _ = fs::remove_dir_all(&base);
64 println!("committed: {:?}", receipt.disposition());
65 Ok(())
66}24fn main() -> Result<(), Box<dyn std::error::Error>> {
25 let base = temp_root("eggup-ownership");
26 let inputs = base.join("inputs");
27 let root = base.join("install");
28 fs::create_dir_all(&inputs)?;
29 fs::create_dir_all(root.join("bin"))?;
30 fs::write(inputs.join("app"), b"new-app")?;
31 fs::write(root.join("bin/app"), b"old-app")?;
32 let new_digest = eggup_core::hash_file(&inputs.join("app"))?;
33 let old_digest = eggup_core::hash_file(&root.join("bin/app"))?;
34 let member = ArtifactMember::new(MemberId::new("main")?, inputs.join("app"), "bin/app")?
35 .with_integrity(IntegrityRequirement::Sha256(new_digest));
36 let plan = InstallPlan::new(
37 ProductId::new("demo")?,
38 ReleaseId::new("4.0.0")?,
39 &root,
40 ArtifactSet::single(member)?,
41 )?;
42 // Prove the live file is the expected old deployment; deny creation of
43 // absent destinations (replacement-only policy).
44 let verifier = ExactDigestVerifier::new(vec![(MemberId::new("main")?, old_digest)]);
45 let receipt = plan
46 .prepare()?
47 .verify_integrity()?
48 .validate(&AllValidators::new())?
49 .commit(CommitOwnership::new(&verifier, AbsentPolicy::DenyCreate))?;
50 assert_eq!(receipt.disposition(), TransactionDisposition::Committed);
51 assert_eq!(fs::read(root.join("bin/app"))?, b"new-app");
52 let _ = fs::remove_dir_all(&base);
53 println!("ownership-verified commit: {:?}", receipt.disposition());
54 Ok(())
55}24fn main() -> Result<(), Box<dyn std::error::Error>> {
25 let base = temp_root("eggup-multi-member");
26 let inputs = base.join("inputs");
27 let root = base.join("install");
28 fs::create_dir_all(&inputs)?;
29 fs::create_dir_all(root.join("bin"))?;
30 fs::write(inputs.join("main"), b"new-main")?;
31 fs::write(inputs.join("helper"), b"new-helper")?;
32 fs::write(root.join("bin/main"), b"old-main")?;
33 fs::write(root.join("bin/helper"), b"old-helper")?;
34 let main_digest = eggup_core::hash_file(&inputs.join("main"))?;
35 let helper_digest = eggup_core::hash_file(&inputs.join("helper"))?;
36 let members = ArtifactSet::new(vec![
37 ArtifactMember::new(MemberId::new("main")?, inputs.join("main"), "bin/main")?
38 .with_integrity(IntegrityRequirement::Sha256(main_digest)),
39 ArtifactMember::new(
40 MemberId::new("helper")?,
41 inputs.join("helper"),
42 "bin/helper",
43 )?
44 .with_integrity(IntegrityRequirement::Sha256(helper_digest)),
45 ])?;
46 let plan = InstallPlan::new(
47 ProductId::new("bundle")?,
48 ReleaseId::new("2.0.0")?,
49 &root,
50 members,
51 )?;
52 let receipt = plan
53 .prepare()?
54 .verify_integrity()?
55 .validate(&AllValidators::new())?
56 .commit(CommitOwnership::new(
57 &ExistingAsOwnedVerifier,
58 AbsentPolicy::AllowCreate,
59 ))?;
60 assert_eq!(receipt.disposition(), TransactionDisposition::Committed);
61 assert_eq!(fs::read(root.join("bin/main"))?, b"new-main");
62 assert_eq!(fs::read(root.join("bin/helper"))?, b"new-helper");
63 let _ = fs::remove_dir_all(&base);
64 println!("multi-member commit: {:?}", receipt.disposition());
65 Ok(())
66}61fn main() -> Result<(), Box<dyn std::error::Error>> {
62 // 1. Committed: success.
63 let (base, plan) = build(b"new", Some(b"old"))?;
64 let receipt = plan
65 .prepare()?
66 .verify_integrity()?
67 .validate(&AllValidators::new())?
68 .commit(CommitOwnership::new(
69 &ExistingAsOwnedVerifier,
70 AbsentPolicy::AllowCreate,
71 ))?;
72 assert_eq!(receipt.disposition(), TransactionDisposition::Committed);
73 assert!(receipt.failure().is_none());
74 println!("committed: {:?}", receipt.disposition());
75 let _ = fs::remove_dir_all(&base);
76
77 // 2. RolledBack: ownership denied, live state unchanged, failure explains why.
78 let (base, plan) = build(b"new", Some(b"old"))?;
79 let root = base.join("install");
80 let receipt = plan
81 .prepare()?
82 .verify_integrity()?
83 .validate(&AllValidators::new())?
84 .commit(CommitOwnership::new(&DenyAll, AbsentPolicy::AllowCreate))?;
85 assert_eq!(receipt.disposition(), TransactionDisposition::RolledBack);
86 assert!(receipt.rollback_verified());
87 let failure = receipt.failure().expect("rolled back carries a cause");
88 println!(
89 "rolled back: phase={} category={} detail={}",
90 failure.phase().as_str(),
91 failure.category().as_str(),
92 failure.detail()
93 );
94 assert_eq!(fs::read(root.join("bin/app"))?, b"old");
95 assert_ne!(
96 receipt.disposition(),
97 TransactionDisposition::Committed,
98 "rollback is not success"
99 );
100 let _ = fs::remove_dir_all(&base);
101
102 // 3. RecoveryRequired is distinct and high severity: it means Eggup cannot
103 // prove either generation is coherent. It is produced by rollback failure
104 // or unclean finalization; operators must inspect `recovery_path` and the
105 // preserved lock. See `docs/transaction.md` for the full contract.
106 println!("recovery-required: inspect recovery_path + lock; do not retry blindly");
107
108 // 4. A failed post-commit check can keep the coherent new generation,
109 // while remaining machine-readable as a failed check.
110 let (base, plan) = build(b"new", Some(b"old"))?;
111 let receipt = plan
112 .prepare()?
113 .verify_integrity()?
114 .validate(&AllValidators::new())?
115 .commit_with_post_commit(
116 CommitOwnership::new(&ExistingAsOwnedVerifier, AbsentPolicy::AllowCreate),
117 PostCommitFailurePolicy::KeepInstalled,
118 || Err("example health check failed"),
119 )?;
120 assert_eq!(receipt.disposition(), TransactionDisposition::Committed);
121 assert!(receipt.post_commit_failure().is_some());
122 println!(
123 "kept installed after failed check: {:?}",
124 receipt.disposition()
125 );
126 let _ = fs::remove_dir_all(base);
127 Ok(())
128}Sourcepub fn prepare_with_bound_sources(
self,
bound: BoundSources,
) -> Result<PreparedTransaction>
pub fn prepare_with_bound_sources( self, bound: BoundSources, ) -> Result<PreparedTransaction>
Copies every member into private owned stage state, sourcing bound members from their already-open objects without any pathname lookup.
Each member identity present in bound is staged by reading the moved
open object from byte zero; its recorded source path is treated as
advisory diagnostics only and is never opened, even if it now names
foreign state. Members absent from bound stage from their recorded
source paths exactly as in prepare.
Every bound identity must name a member of this plan: leftover handles fail closed and no staged byte is ever obtained by reopening a member name as fallback. A failed bound stage copy never falls back to the recorded path.