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
25fn main() -> Result<(), Box<dyn std::error::Error>> {
26 let base = temp_root("eggup-one-member");
27 let inputs = base.join("inputs");
28 let root = base.join("install");
29 fs::create_dir_all(inputs.join("bin"))?;
30 fs::create_dir_all(root.join("bin"))?;
31 fs::write(inputs.join("app"), b"new-app")?;
32 let digest = eggup_core::hash_file(&inputs.join("app"))?;
33 let member = ArtifactMember::new(MemberId::new("main")?, inputs.join("app"), "bin/app")?
34 .with_integrity(IntegrityRequirement::Sha256(digest));
35 let plan = InstallPlan::new(
36 ProductId::new("demo")?,
37 ReleaseId::new("1.0.0")?,
38 &root,
39 ArtifactSet::single(member)?,
40 )?;
41 let receipt = plan
42 .prepare()?
43 .verify_integrity()?
44 .validate(&AllValidators::new())?
45 .commit(CommitOwnership::new(
46 &ExistingAsOwnedVerifier,
47 AbsentPolicy::AllowCreate,
48 ))?;
49 assert_eq!(receipt.disposition(), TransactionDisposition::Committed);
50 assert_eq!(fs::read(root.join("bin/app"))?, b"new-app");
51 let _ = fs::remove_dir_all(&base);
52 println!("one-member commit: {:?}", receipt.disposition());
53 Ok(())
54}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}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 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?
25fn main() -> Result<(), Box<dyn std::error::Error>> {
26 let base = temp_root("eggup-one-member");
27 let inputs = base.join("inputs");
28 let root = base.join("install");
29 fs::create_dir_all(inputs.join("bin"))?;
30 fs::create_dir_all(root.join("bin"))?;
31 fs::write(inputs.join("app"), b"new-app")?;
32 let digest = eggup_core::hash_file(&inputs.join("app"))?;
33 let member = ArtifactMember::new(MemberId::new("main")?, inputs.join("app"), "bin/app")?
34 .with_integrity(IntegrityRequirement::Sha256(digest));
35 let plan = InstallPlan::new(
36 ProductId::new("demo")?,
37 ReleaseId::new("1.0.0")?,
38 &root,
39 ArtifactSet::single(member)?,
40 )?;
41 let receipt = plan
42 .prepare()?
43 .verify_integrity()?
44 .validate(&AllValidators::new())?
45 .commit(CommitOwnership::new(
46 &ExistingAsOwnedVerifier,
47 AbsentPolicy::AllowCreate,
48 ))?;
49 assert_eq!(receipt.disposition(), TransactionDisposition::Committed);
50 assert_eq!(fs::read(root.join("bin/app"))?, b"new-app");
51 let _ = fs::remove_dir_all(&base);
52 println!("one-member commit: {:?}", receipt.disposition());
53 Ok(())
54}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}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.