Skip to content

Commit ab50a32

Browse files
committed
composefs: Remove /etc/.updated on deployment initialization and finalization
ostree explicitly unlinks /etc/.updated (and /var/.updated) when finalizing a new deployment so that systemd ConditionNeedsUpdate=|/etc services like systemd-sysusers and systemd-tmpfiles always run on the first boot of that deployment. The native composefs path was missing this step. initialize_state() copies /etc from the container image with 'cp -a', which preserves any /etc/.updated stamp from the build environment. composefs_backend_finalize() merges /etc into the staged deployment directory but similarly never removes the stamp. The consequence is that systemd sees /etc/.updated already present and concludes /etc needs no update, causing sysusers (and tmpfiles) to be skipped entirely on the first boot of an upgraded deployment. Assisted-by: OpenCode (Claude Sonnet 4.6) Signed-off-by: Colin Walters <walters@verbum.org>
1 parent 83a7c9f commit ab50a32

2 files changed

Lines changed: 21 additions & 3 deletions

File tree

crates/lib/src/bootc_composefs/finalize.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,13 @@ pub(crate) async fn composefs_backend_finalize(
118118
let diff = compute_diff(&pristine_files, &current_files, &new_files)?;
119119
merge(&current_etc, &current_files, &new_etc, &new_files, &diff)?;
120120

121+
// Remove /etc/.updated from the new deployment so that ConditionNeedsUpdate=|/etc
122+
// services (systemd-sysusers, systemd-tmpfiles) run on the first boot, mirroring
123+
// what ostree does in sysroot_finalize_deployment.
124+
new_etc
125+
.remove_file_optional(".updated")
126+
.context("Removing /etc/.updated from staged deployment")?;
127+
121128
// Unmount EROFS
122129
drop(erofs_tmp_mnt);
123130

crates/lib/src/bootc_composefs/state.rs

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -132,16 +132,27 @@ pub(crate) fn initialize_state(
132132
.run_capture_stderr()?;
133133
}
134134

135-
let cp_ret = Command::new("cp")
135+
Command::new("cp")
136136
.args([
137137
"-a",
138138
"--remove-destination",
139139
&format!("{}/etc/.", tempdir.dir.path().as_str()?),
140140
&format!("{state_path}/etc/."),
141141
])
142-
.run_capture_stderr();
142+
.run_capture_stderr()?;
143+
144+
// Remove /etc/.updated so that ConditionNeedsUpdate=|/etc services
145+
// (e.g. systemd-sysusers, systemd-tmpfiles) run on the first boot of
146+
// this deployment, mirroring what ostree does in sysroot_finalize_deployment.
147+
// Without this, systemd sees /etc/.updated from the container image and
148+
// concludes /etc is already up-to-date, causing sysusers to be skipped.
149+
let state_etc = Dir::open_ambient_dir(format!("{state_path}/etc"), ambient_authority())
150+
.context("Opening state etc dir")?;
151+
state_etc
152+
.remove_file_optional(".updated")
153+
.context("Removing /etc/.updated")?;
143154

144-
cp_ret
155+
Ok(())
145156
}
146157

147158
/// Adds or updates the provided key/value pairs in the .origin file of the deployment pointed to

0 commit comments

Comments
 (0)