From 386fd87b5dd7b88348ae76a77d6dd5ffc510e4df Mon Sep 17 00:00:00 2001 From: Kathryn Baldauf Date: Tue, 16 Sep 2025 10:52:08 -0700 Subject: [PATCH] Enumerate using relative paths to avoid mismatch with symlink resolution of special paths like /tmp (#613) ## Type of Change - [x] Bug fix - [ ] New feature - [ ] Breaking change - [ ] Documentation update ## Motivation and Context Fixes https://github.com/apple/container/issues/588. This PR changes the archiver compression file enumeration to use the [enumerator(atPath:)](https://developer.apple.com/documentation/foundation/filemanager/enumerator(atpath:)) version. This version returns relative paths instead of full file paths from the filesystem. /tmp is symlinked to /private/tmp and some swift packages will handle that path differently. While a call to Foundation's `URL.resolvingSymlinksInPath()` will return "/tmp", a call to `FileManager.enumerator(at:)` will return "/private/tmp". This difference causes a container image build to fail when the user is using a path under /tmp or other special case paths as the context directory. ## Testing - [x] Tested locally - [x] Added/updated tests - [ ] Added/updated docs Signed-off-by: Kathryn Baldauf --- Sources/ContainerClient/Archiver.swift | 8 +++---- .../Subcommands/Build/CLIBuilderTest.swift | 21 +++++++++++++++++++ 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/Sources/ContainerClient/Archiver.swift b/Sources/ContainerClient/Archiver.swift index be57de3b..46525ec8 100644 --- a/Sources/ContainerClient/Archiver.swift +++ b/Sources/ContainerClient/Archiver.swift @@ -60,10 +60,7 @@ public final class Archiver: Sendable { try fileManager.createDirectory(at: directory, withIntermediateDirectories: true) guard - let enumerator = FileManager.default.enumerator( - at: source, - includingPropertiesForKeys: [.isDirectoryKey, .isRegularFileKey, .isSymbolicLinkKey] - ) + let enumerator = FileManager.default.enumerator(atPath: source.path) else { throw Error.fileDoesNotExist(source) } @@ -74,7 +71,8 @@ public final class Archiver: Sendable { entryInfo.append(info) } } else { - while let url = enumerator.nextObject() as? URL { + while let relPath = enumerator.nextObject() as? String { + let url = source.appending(path: relPath).standardizedFileURL guard let info = closure(url) else { continue } diff --git a/Tests/CLITests/Subcommands/Build/CLIBuilderTest.swift b/Tests/CLITests/Subcommands/Build/CLIBuilderTest.swift index 2e0ffc42..986e316a 100644 --- a/Tests/CLITests/Subcommands/Build/CLIBuilderTest.swift +++ b/Tests/CLITests/Subcommands/Build/CLIBuilderTest.swift @@ -94,6 +94,27 @@ extension TestCLIBuildBase { #expect(try self.inspectImage(newImageName) == newImageName, "expected to have successfully built \(newImageName)") } + @Test func testBuildAddFromSpecialDirs() throws { + let tempDir = URL(filePath: "/tmp/container/.clitests" + testUUID) + try! FileManager.default.createDirectory(at: tempDir, withIntermediateDirectories: true) + + defer { + try! FileManager.default.removeItem(at: tempDir) + } + + let dockerfile: String = + """ + FROM scratch + + ADD emptyFile / + """ + let context: [FileSystemEntry] = [.file("emptyFile", content: .zeroFilled(size: 1))] + try createContext(tempDir: tempDir, dockerfile: dockerfile, context: context) + let imageName = "registry.local/scratch-add-special-dir:\(UUID().uuidString)" + try self.build(tag: imageName, tempDir: tempDir) + #expect(try self.inspectImage(imageName) == imageName, "expected to have successfully built \(imageName)") + } + @Test func testBuildScratchAdd() throws { let tempDir: URL = try createTempDir() let dockerfile: String =