Skip to content

Commit 1ad347e

Browse files
Liubov Dmitrievameta-codesync[bot]
authored andcommitted
[antlir2] Defer whiteout markers to prevent duplicate tar entries
Summary: When a file was deleted and then recreated/modified in the same OCI layer, we were writing both the whiteout marker `.wh.filename` (for deletion) and the actual `filename` (for creation) to the tar archive. This caused "file exists" errors during podman/skopeo layer extraction because both entries would be present. Example scenario that was broken: 1. Base layer contains /foo 2. Intermediate layer: Unlink /foo → queued .wh.foo for writing 3. Same layer: Create /foo (or Chmod/Chown/SetXattr/etc on /foo) → created new /foo entry 4. Result: tar contained BOTH .wh.foo AND foo → "file exists" error The previous approach wrote whiteout markers immediately when seeing Unlink/Rmdir, but this created duplicates when files were later recreated or modified in the same layer. Fixed by deferring whiteout marker creation until the end of the change stream processing. We track pending whiteouts in a HashSet and remove entries from it whenever we see ANY operation that touches the file (Create, Mkdir, Mkfifo, Mknod, Chmod, Chown, HardLink, Symlink, Rename, Contents, SetXattr, RemoveXattr). Only files that remain in the set at the end get their whiteout markers written to the tar. Additionally, we skip redundant nested whiteouts: if a parent directory is being deleted, we do not write whiteout markers for its children since the parent whiteout already handles the entire subtree. This reduces tar size and improves efficiency. Test Plan: buck test fbcode//mode/dev fbcode//antlir/antlir2/test_images/package/oci:test Large image e2e (something inside the image didn't work but the image started!!!) **After** ```after liuba ⛅️ ~/fbsource/fbcode [🥝] → buck run 'mode/opt' //atlas/specs/generated/fbcode-full:devcompute.image.fbcode-full-podman-run Buck UI: https://www.internalfb.com/buck2/3b8646a4-a51b-46a0-a599-a7a8b25a154f Network: Up: 105GiB Down: 149KiB (reSessionID-2f3c1f02-27ff-4819-8d86-de7b1af7e4c2) Analyzing targets. Remaining 0/8 Executing actions. Remaining 0/116 1:34:57.2s exec time total Command: run. Finished 27 local Time elapsed: 20:20.0s BUILD SUCCEEDED - starting your binary Loading image... Getting image source signatures Copying blob c4e59db8ee34 done | Copying blob 352fb7da57a6 skipped: already exists Copying blob 819addfec0ba skipped: already exists Copying blob 524414b72c1c done | Copying blob c018696d1208 done | Copying blob d0879ca7baef done | Copying blob 0c74cf4b7e4e skipped: already exists Copying blob 076737ae98d0 done | Copying blob dc25d1b96a0e skipped: already exists Copying blob a2e6661378f5 done | Copying blob 714aa1e55577 done | Copying blob 3885446344d9 skipped: already exists Copying blob 243ddc9de428 skipped: already exists Copying blob 5e846f72e45f done | Copying blob 5c106d009435 done | Copying blob c744f8da2387 done | Copying config 52bdc0bc17 done | Writing manifest to image destination Loaded image 52bdc0bc173d8f927bd719e89c6bb3268951c3f0250f146ebec48a131c445351 Starting container... _ _ _ / \ | |_| | __ _ ___ / _ \ | __| |/ _` / __| / ___ \ | |_| | (_| \__ \ /_/ \_\ \__|_|\__,_|___/ I1209 07:13:02.372143 2031590 fbcode/atlas/specs/environment/main.rs:130] Running container from image: 52bdc0bc173d8f927bd719e89c6bb3268951c3f0250f146ebec48a131c445351 I1209 07:13:02.374144 2031590 fbcode/atlas/specs/environment/platform_bind_mounts.rs:159] Adding read-only bind mount: /etc/fbwhoami - FB identity information I1209 07:13:02.374157 2031590 fbcode/atlas/specs/environment/platform_bind_mounts.rs:159] Adding read-only bind mount: /etc/hosts - Hostname resolution I1209 07:13:02.374161 2031590 fbcode/atlas/specs/environment/platform_bind_mounts.rs:159] Adding read-only bind mount: /etc/resolv.conf - DNS configuration I1209 07:13:02.374170 2031590 fbcode/atlas/specs/environment/platform_bind_mounts.rs:159] Adding read-only bind mount: /etc/smc.tiers - SMC tier configuration I1209 07:13:02.374181 2031590 fbcode/atlas/specs/environment/platform_bind_mounts.rs:159] Adding read-write bind mount: /mnt/gvfs - GVFS mounts for source control I1209 07:13:02.374190 2031590 fbcode/atlas/specs/environment/platform_bind_mounts.rs:159] Adding read-write bind mount: /sys/fs/cgroup - cgroup hierarchy (RW safe with private cgroupns) I1209 07:13:02.374198 2031590 fbcode/atlas/specs/environment/platform_bind_mounts.rs:159] Adding read-only bind mount: /usr/local/fbcode - FB code binaries I1209 07:13:02.374206 2031590 fbcode/atlas/specs/environment/platform_bind_mounts.rs:159] Adding read-only bind mount: /var/facebook/rootcanal - Root canal certificates I1209 07:13:02.374214 2031590 fbcode/atlas/specs/environment/platform_bind_mounts.rs:159] Adding read-write bind mount: /var/spool/mcrouter - mcrouter async spool directory I1209 07:13:02.374221 2031590 fbcode/atlas/specs/environment/platform_bind_mounts.rs:159] Adding read-only bind mount: /run/casd - CASd socket directory I1209 07:13:02.374228 2031590 fbcode/atlas/specs/environment/platform_bind_mounts.rs:159] Adding read-only bind mount: /etc/devserver.owners - Devserver owner list I1209 07:13:02.374235 2031590 fbcode/atlas/specs/environment/platform_bind_mounts.rs:159] Adding read-only bind mount: /etc/devserver.whoami - Devserver configuration I1209 07:13:02.374242 2031590 fbcode/atlas/specs/environment/platform_bind_mounts.rs:159] Adding read-write bind mount: /run/wds - WDS runtime directory I1209 07:13:02.374249 2031590 fbcode/atlas/specs/environment/platform_bind_mounts.rs:159] Adding read-only bind mount: /usr/facebook/falcon/bin - Falcon binaries I1209 07:13:02.374256 2031590 fbcode/atlas/specs/environment/platform_bind_mounts.rs:159] Adding read-only bind mount: /usr/local/fbprojects/packages - FB project packages I1209 07:13:02.374265 2031590 fbcode/atlas/specs/environment/platform_bind_mounts.rs:159] Adding read-only bind mount: /var/facebook/configerator-client-system-configs - Configerator system configs I1209 07:13:02.374271 2031590 fbcode/atlas/specs/environment/platform_bind_mounts.rs:159] Adding read-only bind mount: /var/facebook/configerator-mount - Configerator mount point I1209 07:13:02.374282 2031590 fbcode/atlas/specs/environment/platform_bind_mounts.rs:159] Adding read-only bind mount: /var/facebook/configerator-mount/client - Configerator client configs I1209 07:13:02.374302 2031590 fbcode/atlas/specs/environment/platform_bind_mounts.rs:159] Adding read-write bind mount: /var/facebook/hhvm - HHVM runtime directory I1209 07:13:02.374309 2031590 fbcode/atlas/specs/environment/platform_bind_mounts.rs:159] Adding read-only bind mount: /var/facebook/sah - SAH directory I1209 07:13:02.374318 2031590 fbcode/atlas/specs/environment/platform_bind_mounts.rs:172] Using dev environment bind mounts (20 total: 15 RO, 5 RW) I1209 07:13:02.374324 2031590 fbcode/atlas/specs/environment/main.rs:161] Generating certificates for service identity: atlas_container_test I1209 07:13:02.374331 2031590 fbcode/atlas/specs/environment/lib/security/generator.rs:74] Generating Thrift certificate for container: 3b8707e2-60b2-4867-bf84-413076e8c210 I1209 07:13:02.374355 2031590 fbcode/atlas/specs/environment/lib/security/generator.rs:99] Generating thrift certificate for service identity: atlas_container_test I1209 07:13:02.374365 2031590 fbcode/atlas/specs/environment/lib/security/generator.rs:218] Creating certificate directory: /data/security/atlas_certs/3b8707e2-60b2-4867-bf84-413076e8c210/thrift I1209 07:13:03.306023 2031590 fbcode/atlas/specs/environment/lib/security/generator.rs:146] tlscertreq failed as root, attempting fallback to SUDO_USER: liubovd I1209 07:13:05.126447 2031590 fbcode/atlas/specs/environment/lib/security/generator.rs:193] Successfully generated thrift certificates in: /data/security/atlas_certs/3b8707e2-60b2-4867-bf84-413076e8c210/thrift I1209 07:13:05.126491 2031590 fbcode/atlas/specs/environment/security_opts.rs:69] Certificates generated successfully I1209 07:13:05.126502 2031590 fbcode/atlas/specs/environment/main.rs:218] Atlas Environment Id is: 3b8707e2-60b2-4867-bf84-413076e8c210 I1209 07:13:05.126531 2031590 fbcode/atlas/specs/environment/main.rs:250] Running container... I1209 07:13:05.238623 2031590 fbcode/atlas/specs/environment/main.rs:264] Container started with ID: 5ece27980e33df6185fd984a5eae746e9c37d16df3812c0817bcef04e0538701 I1209 07:13:05.879179 2031590 fbcode/atlas/specs/environment/main.rs:338] Running component-manager for envspec: fbcode-full Built by: twsvcscm Built on: Sun Dec 7 09:11:09 2025 (1765127469) Built at: bd52-0671-0004-0000.twshared106320.15.frc2.tw.fbinfra.net Build path: /data/sandcastle/boxes/trunk-hg-fbcode-fbsource Package Name: fb-devenv-component-manager Package Version: 20251207 Package Release: 090244 Build Revision: 1e662ac67ed049b6e88e5894c1447bbc1d5b61e2 Build Revision Timestamp: 1765096877 Build Upstream Revision: 1e662ac67ed049b6e88e5894c1447bbc1d5b61e2 Build Upstream Revision Timestamp: 1765096877 Build Platform: platform010 Build Rule: fbcode:devenv/component-manager:component-manager (rust_binary, buck2, opt) Build Library Versions: W1209 07:13:06.168869 284 api.cpp:841] config data_security_systems/certificate_service/prod/mrl_simple_predicates possesses a signature but this api instance has not been initialized with a ConfigSignatureVerifier object to verify it. Please initialize this api instance with an appropriate ConfigSignatureVerifier Caused by: Failed to run systemd-run command --unit=hookctl-atlas_preparer-setup-atlas_preparer.service --remain-after-exit --property=StandardOutput=truncate:/usr/share/hookctl/runs/atlas_preparer-setup-atlas_preparer-stdout.log --property=StandardError=truncate:/usr/share/hookctl/runs/atlas_preparer-setup-atlas_preparer-stderr.log --property=KillMode=process hookctl run --tool atlas-preparer setup --options {"flavour":{"repo_flavour":{"repo":{"name":"fbsource"},"commit_specifier":{"bookmark_name":"master"}},"additional_capabilities":[],"warmup":null},"session_id":"","invocation_id":"","container_id":""} --context {"requested_devcomponents":["atlas-preparer"],"env_variables":{"ATLAS_CONTAINER_ID":"5ece27980e33df6185fd984a5eae746e9c37d16df3812c0817bcef04e0538701","CONTAINER_BOOT_WITH_SYSTEMD":"1"},"devcomponent_outputs":null} --oncall atlasx --output-path /usr/share/hookctl/runs/atlas_preparer-setup-atlas_preparer-output.json with status Some(1) with stderr: Failed to start transient service unit: Transaction for hookctl-atlas_preparer-setup-atlas_preparer.service/start is destructive (systemd-poweroff.service has 'start' job queued, but 'stop' is included in transaction). ]: {("atlas-preparer", "atlas_preparer-setup-atlas_preparer"): Start { restart: false }, ("atlas-preparer", "atlas_preparer-unknown-atlas_preparer"): Skipping} --- Sorry, the command execution failed: --- --- The command: --- podman exec -u root 5ece27980e33df6185fd984a5eae746e9c37d16df3812c0817bcef04e0538701 /usr/libexec/component-manager --port 6627 --run-until-components-ran --fast-poll-interval-seconds 1 atlas --env-spec-id fbcode-full --options '{"env_variables":{"ATLAS_CONTAINER_ID":"5ece27980e33df6185fd984a5eae746e9c37d16df3812c0817bcef04e0538701","CONTAINER_BOOT_WITH_SYSTEMD":"1"},"devcomponent_inputs":{"atlas-preparer":"{\"flavour\":{\"repo_flavour\":{\"repo\":{\"name\":\"fbsource\"},\"commit_specifier\":{\"bookmark_name\":\"master\"}},\"additional_capabilities\":[],\"warmup\":null},\"session_id\":\"\",\"invocation_id\":\"\",\"container_id\":\"\"}"}}' --- Full command output --- I1209 07:13:06.082679 3 RustThriftServer.cpp:308] RustAsyncProcessorFactory overrides createMethodMetadata(): true I1209 07:13:06.082813 3 ServiceFrameworkLight.cpp:789] Status server running on service port 6627 I1209 07:13:06.084084 3 BuildModule.cpp:23] Build Information Built by: twsvcscm Built on: Sun Dec 7 09:11:09 2025 (1765127469) Built at: bd52-0671-0004-0000.twshared106320.15.frc2.tw.fbinfra.net Build path: /data/sandcastle/boxes/trunk-hg-fbcode-fbsource Package Name: fb-devenv-component-manager Package Version: 20251207 Package Release: 090244 Build Revision: 1e662ac67ed049b6e88e5894c1447bbc1d5b61e2 Build Revision Timestamp: 1765096877 Build Upstream Revision: 1e662ac67ed049b6e88e5894c1447bbc1d5b61e2 Build Upstream Revision Timestamp: 1765096877 Build Platform: platform010 Build Rule: fbcode:devenv/component-manager:component-manager (rust_binary, buck2, opt) Build Library Versions: I1209 07:13:06.084187 3 ServiceFrameworkLight.cpp:1214] Calling serve() on component_manager_server port 6627 I1209 07:13:06.084649 284 ThriftServer.cpp:2060] Using randomly generated TLS ticket keys. I1209 07:13:06.085671 284 ThriftServer.cpp:988] Using resource pools on address/port 6627: thrift flag: true, enable gflag: false, disable gflag: false, runtime actions: I1209 07:13:06.095067 284 ThriftServer.cpp:1538] Resource pools configured: 9 W1209 07:13:06.168869 284 api.cpp:841] config data_security_systems/certificate_service/prod/mrl_simple_predicates possesses a signature but this api instance has not been initialized with a ConfigSignatureVerifier object to verify it. Please initialize this api instance with an appropriate ConfigSignatureVerifier I1209 07:13:06.174559 284 SharedSSLContextManager.h:91] Initialized SSL context configs I1209 07:13:06.205016 284 ThriftServer.cpp:746] ThriftServer listening on address/port: [::]:6627 INFO component_manager_service::thrift: Component manager thrift service started on port 6627 I1209 07:13:06.207849 284 SharedSSLContextManager.h:134] Updated Fizz and SSL context configs INFO component_manager_service::component: Slowest component install status check: atlas-preparer took 842.00ns INFO component_manager_service: Running hooks for components INFO component_manager_service::component: Slowest component install status check: atlas-preparer took 300.00ns INFO component_manager: Run failed and 'exit_if_all_ran' is set, exiting loop... Error: Failed to run hooks for components due to [Failed to start hook HookSpec { metadata: HookMetadata { hook_point: Setup, hook_type: Package { package_name: "fb-atlas-preparer", package_version: None }, success_timeout: Some(10800), failure_timeout: Some(300), user: Some(Root) }, name: "atlas_preparer-setup-atlas_preparer", options: {"context": "{\"requested_devcomponents\":[\"atlas-preparer\"],\"env_variables\":{\"ATLAS_CONTAINER_ID\":\"5ece27980e33df6185fd984a5eae746e9c37d16df3812c0817bcef04e0538701\",\"CONTAINER_BOOT_WITH_SYSTEMD\":\"1\"},\"devcomponent_outputs\":null}", "skip_component_install_status_check": "1", "kill_mode": "process", "placeholder_unixname": "twsvcscm", "output_path": "1", "options": "{\"flavour\":{\"repo_flavour\":{\"repo\":{\"name\":\"fbsource\"},\"commit_specifier\":{\"bookmark_name\":\"master\"}},\"additional_capabilities\":[],\"warmup\":null},\"session_id\":\"\",\"invocation_id\":\"\",\"container_id\":\"\"}"} } Caused by: Failed to run systemd-run command --unit=hookctl-atlas_preparer-setup-atlas_preparer.service --remain-after-exit --property=StandardOutput=truncate:/usr/share/hookctl/runs/atlas_preparer-setup-atlas_preparer-stdout.log --property=StandardError=truncate:/usr/share/hookctl/runs/atlas_preparer-setup-atlas_preparer-stderr.log --property=KillMode=process hookctl run --tool atlas-preparer setup --options {"flavour":{"repo_flavour":{"repo":{"name":"fbsource"},"commit_specifier":{"bookmark_name":"master"}},"additional_capabilities":[],"warmup":null},"session_id":"","invocation_id":"","container_id":""} --context {"requested_devcomponents":["atlas-preparer"],"env_variables":{"ATLAS_CONTAINER_ID":"5ece27980e33df6185fd984a5eae746e9c37d16df3812c0817bcef04e0538701","CONTAINER_BOOT_WITH_SYSTEMD":"1"},"devcomponent_outputs":null} --oncall atlasx --output-path /usr/share/hookctl/runs/atlas_preparer-setup-atlas_preparer-output.json with status Some(1) with stderr: Failed to start transient service unit: Transaction for hookctl-atlas_preparer-setup-atlas_preparer.service/start is destructive (systemd-poweroff.service has 'start' job queued, but 'stop' is included in transaction). ]: {("atlas-preparer", "atlas_preparer-setup-atlas_preparer"): Start { restart: false }, ("atlas-preparer", "atlas_preparer-unknown-atlas_preparer"): Skipping} I1209 07:13:07.068046 3 ServiceFrameworkLight.cpp:1321] Calling stop() on component_manager_server I1209 07:13:07.089294 3 ServiceFrameworkLight.cpp:1321] Calling stop() on component_manager_server --- End of command output --- I1209 07:13:07.187838 2031590 fbcode/atlas/specs/environment/main.rs:438] Component manager failed after 1.31s I1209 07:13:07.187875 2031590 fbcode/atlas/specs/environment/main.rs:453] Component manager failed. Cleaning up container... Error: Component manager failed to run successfully Caused by: Command exited with code 1 ``` **Before** - broken with (error: exit status 1; output: file exists) or duplicate entries ```before liuba ⛅️ ~/fbsource/fbcode [🍉] → buck run //atlas/specs/generated/fbcode-full:devcompute.image.fbcode-full-podman-run Buck UI: https://www.internalfb.com/buck2/4250ed75-a981-4240-b921-6313dad25ed8 Network: Up: 107GiB Down: 85GiB (reSessionID-0d00c143-835c-4147-a9a3-328f2abf26b8) Loading targets. Remaining 0/2922 6384 dirs read, 17453 targets declared Analyzing targets. Remaining 0/20343 775506 actions, 1030012 artifacts declared Executing actions. Remaining 0/145644 51:51:39.2s exec time total Command: run. Finished 118 local, 48677 cache (99% hit) 50:00:54.4s exec time cached (96%) Time elapsed: 34:54.4s BUILD SUCCEEDED - starting your binary Loading image... Getting image source signatures Copying blob 0c74cf4b7e4e done | Copying blob 63103bf937a9 done | Copying blob 352fb7da57a6 done | Copying blob e754254f44d2 done | Copying blob ea00d113836d done | Copying blob c9d4c161a41f done | Copying blob 18ca4d2ef1d6 done | Copying blob 6914fbdb5252 done | Copying blob e0de1ea23ea5 done | Copying blob 698e382233c2 done | Copying blob a2e6661378f5 done | Copying blob bcf0f4bc258d done | Copying blob 3885446344d9 done | Copying blob 07265e29c9f4 done | Copying blob 026652bc795e done | Copying blob c744f8da2387 done | Error: payload does not match any of the supported image formats: * oci: open /data/users/liubovd/fbsource/buck-out/v2/gen/fbcode/63a66d9176d56dab/atlas/specs/generated/fbcode-full/__devcompute.image.fbcode-full-docker-archive__/devcompute.image.fbcode-full-docker-archive/index.json: not a directory * oci-archive: loading index: open /var/tmp/container_images_oci1817669356/index.json: no such file or directory * docker-archive: unable to copy from source docker-archive:/data/users/liubovd/fbsource/buck-out/v2/gen/fbcode/63a66d9176d56dab/atlas/specs/generated/fbcode-full/__devcompute.image.fbcode-full-docker-archive__/devcompute.image.fbcode-full-docker-archive:0: writing blob: adding layer with blob "sha256:e0de1ea23ea522b8add91ede2479f7847d30db9acf1b0cf42bfd8d0b57822130"/""/"sha256:e0de1ea23ea522b8add91ede2479f7847d30db9acf1b0cf42bfd8d0b57822130": unpacking failed (error: exit status 1; output: file exists) * dir: open /data/users/liubovd/fbsource/buck-out/v2/gen/fbcode/63a66d9176d56dab/atlas/specs/generated/fbcode-full/__devcompute.image.fbcode-full-docker-archive__/devcompute.image.fbcode-full-docker-archive/manifest.json: not a directory ``` Reviewed By: vmagro Differential Revision: D88746637 fbshipit-source-id: 24320ba6427b9d7e917eac834ffbae8687f1ceeb
1 parent 8887698 commit 1ad347e

File tree

1 file changed

+58
-5
lines changed
  • antlir/antlir2/antlir2_packager/make_oci_layer/src

1 file changed

+58
-5
lines changed

antlir/antlir2/antlir2_packager/make_oci_layer/src/main.rs

Lines changed: 58 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -124,27 +124,37 @@ fn main() -> Result<()> {
124124
// separately track which paths had times set, so we can see if *only* the
125125
// times were updated and skip those entries
126126
let mut had_set_times: HashSet<PathBuf> = HashSet::new();
127+
// Track pending whiteout markers - only write them at the end if the file wasn't recreated
128+
let mut pending_whiteouts: HashSet<PathBuf> = HashSet::new();
127129

128130
for change in stream {
129131
let change = change?;
130132
let path = change.path().to_owned();
131133
match change.into_operation() {
132134
Operation::Create { mode } => {
135+
// File is being created - remove from pending whiteouts if present
136+
pending_whiteouts.remove(&path);
133137
let header = &mut entries.entry(path)?.header;
134138
header.set_mode(mode);
135139
header.set_entry_type(EntryType::Regular);
136140
}
137141
Operation::Mkdir { mode } => {
142+
// Directory is being created - remove from pending whiteouts if present
143+
pending_whiteouts.remove(&path);
138144
let header = &mut entries.entry(path)?.header;
139145
header.set_mode(mode);
140146
header.set_entry_type(EntryType::Directory);
141147
}
142148
Operation::Mkfifo { mode } => {
149+
// FIFO is being created - remove from pending whiteouts if present
150+
pending_whiteouts.remove(&path);
143151
let header = &mut entries.entry(path)?.header;
144152
header.set_mode(mode);
145153
header.set_entry_type(EntryType::Fifo);
146154
}
147155
Operation::Mknod { rdev, mode } => {
156+
// Device node is being created - remove from pending whiteouts if present
157+
pending_whiteouts.remove(&path);
148158
let header = &mut entries.entry(path)?.header;
149159
header.set_mode(mode);
150160
let sflag = SFlag::from_bits_truncate(mode);
@@ -157,10 +167,14 @@ fn main() -> Result<()> {
157167
header.set_device_minor(minor(rdev) as u32)?;
158168
}
159169
Operation::Chmod { mode } => {
170+
// Permissions are being modified - file still exists, remove from pending whiteouts
171+
pending_whiteouts.remove(&path);
160172
let header = &mut entries.entry(path)?.header;
161173
header.set_mode(mode);
162174
}
163175
Operation::Chown { uid, gid } => {
176+
// Ownership is being modified - file still exists, remove from pending whiteouts
177+
pending_whiteouts.remove(&path);
164178
let header = &mut entries.entry(path)?.header;
165179
header.set_uid(uid as u64);
166180
header.set_gid(gid as u64);
@@ -170,31 +184,43 @@ fn main() -> Result<()> {
170184
had_set_times.insert(path.clone());
171185
}
172186
Operation::HardLink { target } => {
187+
// Link is being created - remove from pending whiteouts if present
188+
pending_whiteouts.remove(&path);
173189
let entry = entries.entry(path)?;
174190
entry.header.set_entry_type(EntryType::Link);
175191
entry.contents = Contents::Link(target.to_owned());
176192
}
177193
Operation::Symlink { target } => {
194+
// Symlink is being created - remove from pending whiteouts if present
195+
pending_whiteouts.remove(&path);
178196
let entry = entries.entry(path)?;
179197
entry.header.set_entry_type(EntryType::Symlink);
180198
entry.contents = Contents::Link(target.to_owned());
181199
}
182200
Operation::Rename { to: _ } => {
201+
// File is being renamed (recreated) - remove from pending whiteouts if present
202+
pending_whiteouts.remove(&path);
183203
// just ensure an entry exists, which will end up sending the
184204
// full contents, since there is no way to represent a rename in
185205
// the layer tar
186206
entries.entry(path)?;
187207
}
188208
Operation::Contents { contents } => {
209+
// File contents are being set - remove from pending whiteouts if present
210+
pending_whiteouts.remove(&path);
189211
let entry = entries.entry(path)?;
190212
entry.contents = Contents::File(contents);
191213
}
192214
Operation::RemoveXattr { .. } => {
215+
// Xattr is being modified - remove from pending whiteouts if present
216+
pending_whiteouts.remove(&path);
193217
// just ensure an entry exists, which will end up sending the
194218
// full contents
195219
entries.entry(path)?;
196220
}
197221
Operation::SetXattr { name, value } => {
222+
// Xattr is being set - remove from pending whiteouts if present
223+
pending_whiteouts.remove(&path);
198224
let entry = entries.entry(path)?;
199225
let mut key = "SCHILY.xattr.".to_owned();
200226
key.push_str(
@@ -204,12 +230,10 @@ fn main() -> Result<()> {
204230
entry.extensions.push((key, value))
205231
}
206232
// Removals are represented with special whiteout marker files
233+
// We defer writing them until the end to handle the case where
234+
// a file is deleted and then recreated in the same layer
207235
Operation::Unlink | Operation::Rmdir => {
208-
let mut wh_name = OsString::from(".wh.");
209-
wh_name.push(path.file_name().expect("root dir cannot be deleted"));
210-
let wh_path = path.parent().unwrap_or(Path::new("")).join(wh_name);
211-
let mut entry = Entry::default();
212-
builder.append_data(&mut entry.header, wh_path, std::io::empty())?;
236+
pending_whiteouts.insert(path.clone());
213237
}
214238
Operation::Close => {
215239
// we're done with an entry file, it can go into the tar now
@@ -225,6 +249,10 @@ fn main() -> Result<()> {
225249
}
226250
};
227251

252+
// If this file was marked for deletion (whiteout) but is now being
253+
// recreated, remove it from pending whiteouts
254+
pending_whiteouts.remove(&path);
255+
228256
if path == Path::new("") {
229257
// empty path (root) can't go into the tar
230258
continue;
@@ -295,6 +323,31 @@ fn main() -> Result<()> {
295323
}
296324
}
297325

326+
// Write all pending whiteout markers for files that were deleted and not recreated.
327+
// Skip redundant nested whiteouts - if a parent directory is being deleted,
328+
// we don't need whiteout markers for its children.
329+
for wh_path in &pending_whiteouts {
330+
// Check if any ancestor of this path is also being deleted
331+
let has_deleted_ancestor = wh_path
332+
.ancestors()
333+
.skip(1) // Skip the path itself
334+
.any(|ancestor| pending_whiteouts.contains(ancestor));
335+
336+
if has_deleted_ancestor {
337+
// Parent directory is being deleted, so this child whiteout is redundant
338+
continue;
339+
}
340+
341+
let mut wh_name = OsString::from(".wh.");
342+
wh_name.push(wh_path.file_name().expect("root dir cannot be deleted"));
343+
let wh_full_path = wh_path.parent().unwrap_or(Path::new("")).join(wh_name);
344+
let mut header = Header::new_ustar();
345+
header.set_mtime(FIXED_MTIME);
346+
header.set_mode(0o644);
347+
header.set_entry_type(EntryType::Regular);
348+
builder.append_data(&mut header, wh_full_path, std::io::empty())?;
349+
}
350+
298351
ensure!(
299352
entries.is_empty(),
300353
"not all entries were closed: {}",

0 commit comments

Comments
 (0)