From 37fc68fee091070be1b15579e2362164ae0d762d Mon Sep 17 00:00:00 2001 From: Kathryn Baldauf Date: Wed, 11 Jun 2025 13:12:10 -0700 Subject: [PATCH 01/45] Add the option to disable progress updates on image CLI calls (#146) Closes https://github.com/apple/container/issues/126 This PR additionally removes the ability to set `disable-progress-updates` for `container create` calls while we investigate why output get jumbled there. Signed-off-by: Kathryn Baldauf --- Sources/CLI/Container/ContainerCreate.swift | 17 +++++--------- Sources/CLI/Image/ImagePull.swift | 25 +++++++++++++++------ Sources/CLI/Image/ImagePush.swift | 22 ++++++++++++------ Sources/CLI/RunCommand.swift | 7 ++++-- Sources/ContainerClient/Flags.swift | 8 +++++++ 5 files changed, 52 insertions(+), 27 deletions(-) diff --git a/Sources/CLI/Container/ContainerCreate.swift b/Sources/CLI/Container/ContainerCreate.swift index de2247650..5071c10e6 100644 --- a/Sources/CLI/Container/ContainerCreate.swift +++ b/Sources/CLI/Container/ContainerCreate.swift @@ -48,17 +48,12 @@ extension Application { var global: Flags.Global func run() async throws { - var progressConfig: ProgressConfig - if managementFlags.disableProgressUpdates { - progressConfig = try ProgressConfig(disableProgressUpdates: managementFlags.disableProgressUpdates) - } else { - progressConfig = try ProgressConfig( - showTasks: true, - showItems: true, - ignoreSmallSize: true, - totalTasks: 3 - ) - } + let progressConfig = try ProgressConfig( + showTasks: true, + showItems: true, + ignoreSmallSize: true, + totalTasks: 3 + ) let progress = ProgressBar(config: progressConfig) defer { progress.finish() diff --git a/Sources/CLI/Image/ImagePull.swift b/Sources/CLI/Image/ImagePull.swift index ade652a85..58f6dc2c6 100644 --- a/Sources/CLI/Image/ImagePull.swift +++ b/Sources/CLI/Image/ImagePull.swift @@ -34,15 +34,19 @@ extension Application { @OptionGroup var registry: Flags.Registry + @OptionGroup + var progressFlags: Flags.Progress + @Option(help: "Platform string in the form 'os/arch/variant'. Example 'linux/arm64/v8', 'linux/amd64'") var platform: String? @Argument var reference: String init() {} - init(platform: String? = nil, scheme: String = "auto", reference: String) { + init(platform: String? = nil, scheme: String = "auto", reference: String, disableProgress: Bool = false) { self.global = Flags.Global() self.registry = Flags.Registry(scheme: scheme) + self.progressFlags = Flags.Progress(disableProgressUpdates: disableProgress) self.platform = platform self.reference = reference } @@ -56,12 +60,19 @@ extension Application { let scheme = try RequestScheme(registry.scheme) let processedReference = try ClientImage.normalizeReference(reference) - let progressConfig = try ProgressConfig( - showTasks: true, - showItems: true, - ignoreSmallSize: true, - totalTasks: 2 - ) + + var progressConfig: ProgressConfig + if self.progressFlags.disableProgressUpdates { + progressConfig = try ProgressConfig(disableProgressUpdates: self.progressFlags.disableProgressUpdates) + } else { + progressConfig = try ProgressConfig( + showTasks: true, + showItems: true, + ignoreSmallSize: true, + totalTasks: 2 + ) + } + let progress = ProgressBar(config: progressConfig) defer { progress.finish() diff --git a/Sources/CLI/Image/ImagePush.swift b/Sources/CLI/Image/ImagePush.swift index cba664c2d..e61d162de 100644 --- a/Sources/CLI/Image/ImagePush.swift +++ b/Sources/CLI/Image/ImagePush.swift @@ -33,6 +33,9 @@ extension Application { @OptionGroup var registry: Flags.Registry + @OptionGroup + var progressFlags: Flags.Progress + @Option(help: "Platform string in the form 'os/arch/variant'. Example 'linux/arm64/v8', 'linux/amd64'") var platform: String? @Argument var reference: String @@ -46,13 +49,18 @@ extension Application { let scheme = try RequestScheme(registry.scheme) let image = try await ClientImage.get(reference: reference) - let progressConfig = try ProgressConfig( - description: "Pushing image \(image.reference)", - itemsName: "blobs", - showItems: true, - showSpeed: false, - ignoreSmallSize: true - ) + var progressConfig: ProgressConfig + if progressFlags.disableProgressUpdates { + progressConfig = try ProgressConfig(disableProgressUpdates: progressFlags.disableProgressUpdates) + } else { + progressConfig = try ProgressConfig( + description: "Pushing image \(image.reference)", + itemsName: "blobs", + showItems: true, + showSpeed: false, + ignoreSmallSize: true + ) + } let progress = ProgressBar(config: progressConfig) defer { progress.finish() diff --git a/Sources/CLI/RunCommand.swift b/Sources/CLI/RunCommand.swift index 22231801e..822f72058 100644 --- a/Sources/CLI/RunCommand.swift +++ b/Sources/CLI/RunCommand.swift @@ -45,6 +45,9 @@ extension Application { @OptionGroup var global: Flags.Global + @OptionGroup + var progressFlags: Flags.Progress + @Argument(help: "Image name") var image: String @@ -56,8 +59,8 @@ extension Application { let id = Utility.createContainerID(name: self.managementFlags.name) var progressConfig: ProgressConfig - if managementFlags.disableProgressUpdates { - progressConfig = try ProgressConfig(disableProgressUpdates: managementFlags.disableProgressUpdates) + if progressFlags.disableProgressUpdates { + progressConfig = try ProgressConfig(disableProgressUpdates: progressFlags.disableProgressUpdates) } else { progressConfig = try ProgressConfig( showTasks: true, diff --git a/Sources/ContainerClient/Flags.swift b/Sources/ContainerClient/Flags.swift index 12d5bf1c9..0efcbd75b 100644 --- a/Sources/ContainerClient/Flags.swift +++ b/Sources/ContainerClient/Flags.swift @@ -138,6 +138,14 @@ public struct Flags { @Option(name: [.customLong("label"), .customShort("l")], help: "Add a key=value label to the container") public var labels: [String] = [] + } + + public struct Progress: ParsableArguments { + public init() {} + + public init(disableProgressUpdates: Bool) { + self.disableProgressUpdates = disableProgressUpdates + } @Flag(name: .customLong("disable-progress-updates"), help: "Disable progress bar updates") public var disableProgressUpdates = false From 2d262dc0fc7c50c621e233da6d431c476bfbaa8b Mon Sep 17 00:00:00 2001 From: J Logan Date: Wed, 11 Jun 2025 13:21:53 -0700 Subject: [PATCH 02/45] Removes build variable that is not needed after launch. (#151) --- Package.resolved | 2 +- Package.swift | 6 +----- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/Package.resolved b/Package.resolved index 2aa7bdfb6..5fb976ba9 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "ea3144c0718528cd6faef7841a8c1fb6ae9339473ba78f9d5d87b16415d46d58", + "originHash" : "cb5cf6c245fe60f647e4e106917dc2e6559eda5170af37e63e52e549b2eed566", "pins" : [ { "identity" : "async-http-client", diff --git a/Package.swift b/Package.swift index b06883dd5..4a278f61a 100644 --- a/Package.swift +++ b/Package.swift @@ -27,11 +27,7 @@ if let path = ProcessInfo.processInfo.environment["CONTAINERIZATION_PATH"] { scVersion = "latest" } else { scVersion = "0.1.0" - if let containerizationRepo = ProcessInfo.processInfo.environment["CONTAINERIZATION_REPO"], containerizationRepo != "" { - scDependency = .package(url: containerizationRepo, exact: Version(stringLiteral: scVersion)) - } else { - scDependency = .package(url: "https://github.com/apple/containerization.git", exact: Version(stringLiteral: scVersion)) - } + scDependency = .package(url: "https://github.com/apple/containerization.git", exact: Version(stringLiteral: scVersion)) } let releaseVersion = ProcessInfo.processInfo.environment["RELEASE_VERSION"] ?? "0.0.0" From 2327e899cf35932c4ca5c8447489143c58dd2c6a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20St=C3=B6ckle?= Date: Thu, 12 Jun 2025 01:56:39 +0200 Subject: [PATCH 03/45] Remove trailing whitespace from GitHub workflows (#154) Remove trailing whitespace from GitHub workflows --- .github/workflows/build.yml | 4 ++-- .github/workflows/common.yml | 30 +++++++++++++++--------------- .github/workflows/docs-release.yml | 8 ++++---- .github/workflows/release.yml | 8 ++++---- 4 files changed, 25 insertions(+), 25 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 18e1d9c6f..b6821e544 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,6 +1,6 @@ name: container project - PR/merge build -on: +on: pull_request: types: [opened, reopened, synchronize] push: @@ -8,7 +8,7 @@ on: - main - release/* -jobs: +jobs: build: name: Invoke build uses: ./.github/workflows/common.yml diff --git a/.github/workflows/common.yml b/.github/workflows/common.yml index b439222b2..70cc604b1 100644 --- a/.github/workflows/common.yml +++ b/.github/workflows/common.yml @@ -1,15 +1,15 @@ name: container project - common jobs -on: +on: workflow_call: inputs: release: - type: boolean + type: boolean description: "Publish this build for release" default: false jobs: - buildAndTest: + buildAndTest: name: Build and test the project timeout-minutes: 30 runs-on: [self-hosted, macos, sequoia, ARM64] @@ -18,40 +18,40 @@ jobs: packages: read steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v4 with: fetch-depth: 0 - name: Update containerization - run: | + run: | /usr/bin/swift package update containerization - name: Check formatting - run: | + run: | ./scripts/install-hawkeye.sh make fmt - if ! git diff --quiet -- . ':(exclude)Package.swift' ':(exclude)Package.resolved'; then echo "The following files require formatting or license header updates:\n$(git diff --name-only)" ; false ; fi - - name: Check protobuf - run: | - make protos + if ! git diff --quiet -- . ':(exclude)Package.swift' ':(exclude)Package.resolved'; then echo "The following files require formatting or license header updates:\n$(git diff --name-only)" ; false ; fi + - name: Check protobuf + run: | + make protos # TODO [launch]: TEMPORARILY we need to exclude these files since we had to modify them to add - # the github token for pulling the private repos. - if ! git diff --quiet -- . ':(exclude)Package.swift' ':(exclude)Package.resolved' ':(exclude)Protobuf.Makefile'; then echo "The following files require formatting or license header updates:\n$(git diff --name-only)" ; false ; fi + # the github token for pulling the private repos. + if ! git diff --quiet -- . ':(exclude)Package.swift' ':(exclude)Package.resolved' ':(exclude)Protobuf.Makefile'; then echo "The following files require formatting or license header updates:\n$(git diff --name-only)" ; false ; fi env: CURRENT_SDK: y - name: Set build configuration run: | echo "BUILD_CONFIGURATION=debug" >> $GITHUB_ENV - if [ ${{ inputs.release }} == true ]; then + if [ ${{ inputs.release }} == true ]; then echo "BUILD_CONFIGURATION=release" >> $GITHUB_ENV fi - name: Make the container project and docs - run: | + run: | make container dsym docs tar cfz _site.tgz _site env: DEVELOPER_DIR: "/Applications/Xcode_16.3.app/Contents/Developer" CURRENT_SDK: y - - name: Create package + - name: Create package run: | mkdir -p outputs mv bin/${{ env.BUILD_CONFIGURATION }}/container-installer-unsigned.pkg outputs diff --git a/.github/workflows/docs-release.yml b/.github/workflows/docs-release.yml index 8cfb0ec2a..68a19854d 100644 --- a/.github/workflows/docs-release.yml +++ b/.github/workflows/docs-release.yml @@ -1,15 +1,15 @@ -# Manual workflow for releasing docs ad-hoc. Workflow can only be run for main or release branches. +# Manual workflow for releasing docs ad-hoc. Workflow can only be run for main or release branches. # Workflow does NOT publish a release of container. name: Deploy application website -on: +on: workflow_dispatch: -jobs: +jobs: checkBranch: runs-on: ubuntu-latest if: github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags') || startsWith(github.ref, 'refs/heads/release') steps: - - name: Branch validation + - name: Branch validation run: echo "Branch ${{ github.ref_name }} is allowed" buildSite: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 449007b90..98e7a9cbc 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,15 +1,15 @@ name: container project - release build -on: - push: +on: + push: tags: - "[0-9]+.[0-9]+.[0-9]+" -jobs: +jobs: build: name: Invoke build and release uses: ./.github/workflows/common.yml - with: + with: release: true secrets: inherit permissions: From 5f7fe489a64f543aeda537a4a3f3e700c7dc785a Mon Sep 17 00:00:00 2001 From: Kathryn Baldauf Date: Wed, 11 Jun 2025 17:04:03 -0700 Subject: [PATCH 04/45] Add issue templates for bugs and features (#152) I would love feedback on how people feel about these issue templates, if there should be more or less templates, or if there are any fields that people think we should add. You can see an example of how to use these by testing opening an issue on my fork [here](https://github.com/katiewasnothere/container/issues). Signed-off-by: Kathryn Baldauf --- .github/ISSUE_TEMPLATE/01-bug.yml | 78 +++++++++++++++++++++++++++ .github/ISSUE_TEMPLATE/02-feature.yml | 32 +++++++++++ .github/ISSUE_TEMPLATE/config.yml | 5 ++ 3 files changed, 115 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/01-bug.yml create mode 100644 .github/ISSUE_TEMPLATE/02-feature.yml create mode 100644 .github/ISSUE_TEMPLATE/config.yml diff --git a/.github/ISSUE_TEMPLATE/01-bug.yml b/.github/ISSUE_TEMPLATE/01-bug.yml new file mode 100644 index 000000000..a194fc4b6 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/01-bug.yml @@ -0,0 +1,78 @@ +name: Bug Report +description: File a bug report. +title: "[Bug]: " +labels: ["bug", "triage"] +body: + - type: markdown + attributes: + value: | + Thanks for taking the time to fill out this bug report! + - type: checkboxes + id: prereqs + attributes: + label: I have done the following + description: Select that you have completed the following prerequisites. + options: + - label: I have searched the existing issues + required: true + - label: If possible, I've reproduced the issue using the 'main' branch of this project + required: false + - type: input + id: contact + attributes: + label: Contact Details + description: How can we get in touch with you if we need more info? ex. email@example.com + validations: + required: false + - type: textarea + id: reproduce + attributes: + label: Steps to reproduce + description: Explain how to reproduce the incorrect behavior. + validations: + required: true + - type: textarea + id: what-happened + attributes: + label: Current behavior + description: A concise description of what you're experiencing. + validations: + required: true + - type: textarea + id: expected + attributes: + label: Expected Behavior + description: A concise description of what you expected to happen. + validations: + required: true + - type: textarea + attributes: + label: Environment + description: | + examples: + - **OS**: MacOS 26 Beta 1 + - **swift**: Apple Swift version 6.2 + - **xcode**: Xcode 26 Beta 17A5241e + - **container**: container CLI version 0.1.0 + value: | + - OS: + - swift: + - xcode: + - container: + render: markdown + validations: + required: true + - type: textarea + id: logs + attributes: + label: Relevant log output + description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks. + render: shell + - type: checkboxes + id: terms + attributes: + label: Code of Conduct + description: By submitting this issue, you agree to follow our [Code of Conduct](https://github.com/apple/container/blob/main/CONTRIBUTING.md). + options: + - label: I agree to follow this project's Code of Conduct + required: true diff --git a/.github/ISSUE_TEMPLATE/02-feature.yml b/.github/ISSUE_TEMPLATE/02-feature.yml new file mode 100644 index 000000000..ba1548b59 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/02-feature.yml @@ -0,0 +1,32 @@ +name: Feature or Enhancement request +description: File a request for a feature or enhancement +title: "[Request]: " +labels: ["feature", "triage"] +body: + - type: markdown + attributes: + value: | + Thanks for contributing to the container project! + - type: input + id: contact + attributes: + label: Contact Details + description: How can we get in touch with you if we need more info? + placeholder: ex. email@example.com + validations: + required: false + - type: textarea + id: request + attributes: + label: Feature or enhancement request details + description: Describe your proposed feature or enhancement. Code samples that show what's missing, or what new capabilities will be possible, are very helpful! Provide links to existing issues or external references/discussions, if appropriate. + validations: + required: true + - type: checkboxes + id: terms + attributes: + label: Code of Conduct + description: By submitting this issue, you agree to follow our [Code of Conduct](https://example.com). + options: + - label: I agree to follow this project's Code of Conduct + required: true diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 000000000..2dff99170 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,5 @@ +blank_issues_enabled: false +contact_links: + - name: container community support + url: https://github.com/apple/container/discussions + about: Please ask and answer questions here. \ No newline at end of file From 97d4ed6705a81ad4f8ec650b01aee2ef7504a3bc Mon Sep 17 00:00:00 2001 From: Alexander <45719053+KeoFoxy@users.noreply.github.com> Date: Thu, 12 Jun 2025 03:11:42 +0300 Subject: [PATCH 05/45] Updated gitignore: .idea (#138) Added .idea to .gitignore file --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 024b97af5..964c7ce6f 100644 --- a/.gitignore +++ b/.gitignore @@ -14,6 +14,7 @@ workdir/ installer/ .xcode/ .vscode/ +.idea/ .venv/ .clitests/ test_results/ From 3838be520704fb3aa6fcccf64db899ac4c1cdef9 Mon Sep 17 00:00:00 2001 From: Kathryn Baldauf Date: Thu, 12 Jun 2025 10:51:00 -0700 Subject: [PATCH 06/45] Update protos and add builder shim version in Package.swift (#176) This PR updates the protos to match the recent changes in https://github.com/apple/container-builder-shim/pull/15. This PR additionally adds the builder shim version as a variable in Package.swift. This allows us to be consistent with the builder tag used for the builder shim image and when building protobuf files. Signed-off-by: Kathryn Baldauf --- Package.swift | 2 ++ Protobuf.Makefile | 4 +++- Sources/CVersion/Version.c | 4 ++++ Sources/CVersion/include/Version.h | 6 ++++++ Sources/ContainerBuild/Builder.grpc.swift | 16 ++++++++-------- .../ContainerClient/Core/ClientDefaults.swift | 3 ++- 6 files changed, 25 insertions(+), 10 deletions(-) diff --git a/Package.swift b/Package.swift index 4a278f61a..1ca4e9d1a 100644 --- a/Package.swift +++ b/Package.swift @@ -32,6 +32,7 @@ if let path = ProcessInfo.processInfo.environment["CONTAINERIZATION_PATH"] { let releaseVersion = ProcessInfo.processInfo.environment["RELEASE_VERSION"] ?? "0.0.0" let gitCommit = ProcessInfo.processInfo.environment["GIT_COMMIT"] ?? "unspecified" +let builderShimVersion = "0.2.0" let package = Package( name: "container", @@ -309,6 +310,7 @@ let package = Package( .define("CZ_VERSION", to: "\"\(scVersion)\""), .define("GIT_COMMIT", to: "\"\(gitCommit)\""), .define("RELEASE_VERSION", to: "\"\(releaseVersion)\""), + .define("BUILDER_SHIM_VERSION", to: "\"\(builderShimVersion)\"") ] ), ] diff --git a/Protobuf.Makefile b/Protobuf.Makefile index badf6a31f..1966cd010 100644 --- a/Protobuf.Makefile +++ b/Protobuf.Makefile @@ -17,7 +17,9 @@ LOCAL_DIR := $(ROOT_DIR)/.local LOCALBIN := $(LOCAL_DIR)/bin BUILDER_SHIM_REPO ?= https://github.com/apple/container-builder-shim.git + ## Versions +BUILDER_SHIM_VERSION ?= $(shell sed -n 's/let builderShimVersion *= *"\(.*\)"/\1/p' Package.swift) PROTOC_VERSION=26.1 # protoc binary installation @@ -41,7 +43,7 @@ protoc-gen-swift: protos: $(PROTOC) protoc-gen-swift protoc_gen_grpc_swift @echo Generating protocol buffers source code... @mkdir -p $(LOCAL_DIR) - @cd $(LOCAL_DIR) && git clone $(BUILDER_SHIM_REPO) + @cd $(LOCAL_DIR) && git clone --branch $(BUILDER_SHIM_VERSION) --depth 1 $(BUILDER_SHIM_REPO) @$(PROTOC) $(LOCAL_DIR)/container-builder-shim/pkg/api/Builder.proto \ --plugin=protoc-gen-grpc-swift=$(BUILD_BIN_DIR)/protoc-gen-grpc-swift \ --plugin=protoc-gen-swift=$(BUILD_BIN_DIR)/protoc-gen-swift \ diff --git a/Sources/CVersion/Version.c b/Sources/CVersion/Version.c index 38722b5f7..e579378d6 100644 --- a/Sources/CVersion/Version.c +++ b/Sources/CVersion/Version.c @@ -42,3 +42,7 @@ const char* get_release_version() { const char* get_swift_containerization_version() { return CZ_VERSION; } + +const char* get_container_builder_shim_version() { + return BUILDER_SHIM_VERSION; +} diff --git a/Sources/CVersion/include/Version.h b/Sources/CVersion/include/Version.h index 031c209f4..c5b17d194 100644 --- a/Sources/CVersion/include/Version.h +++ b/Sources/CVersion/include/Version.h @@ -40,8 +40,14 @@ #define RELEASE_VERSION "0.0.0" #endif +#ifndef BUILDER_SHIM_VERSION +#define BUILDER_SHIM_VERSION "0.0.0" +#endif + const char* get_git_commit(); const char* get_release_version(); const char* get_swift_containerization_version(); + +const char* get_container_builder_shim_version(); diff --git a/Sources/ContainerBuild/Builder.grpc.swift b/Sources/ContainerBuild/Builder.grpc.swift index eac5bfccf..e742b9557 100644 --- a/Sources/ContainerBuild/Builder.grpc.swift +++ b/Sources/ContainerBuild/Builder.grpc.swift @@ -33,7 +33,7 @@ import SwiftProtobuf /// To perform a build: /// /// 1. CreateBuild to create a new build -/// 2. StartBuild to start the build exection where client and server +/// 2. StartBuild to start the build execution where client and server /// both have a stream for exchanging data during the build. /// /// The client may send: @@ -96,7 +96,7 @@ import SwiftProtobuf /// source path, and empty data /// 2. server archives the data at source path, and starts to send chunks to /// the client -/// 3. server coninues to send all chunks until last chunk, which server +/// 3. server continues to send all chunks until last chunk, which server /// will send with /// 'complete' set to true /// 4. client starts to receive the data and stream to a temporary file @@ -263,7 +263,7 @@ public struct Com_Apple_Container_Build_V1_BuilderNIOClient: Com_Apple_Container /// To perform a build: /// /// 1. CreateBuild to create a new build -/// 2. StartBuild to start the build exection where client and server +/// 2. StartBuild to start the build execution where client and server /// both have a stream for exchanging data during the build. /// /// The client may send: @@ -326,7 +326,7 @@ public struct Com_Apple_Container_Build_V1_BuilderNIOClient: Com_Apple_Container /// source path, and empty data /// 2. server archives the data at source path, and starts to send chunks to /// the client -/// 3. server coninues to send all chunks until last chunk, which server +/// 3. server continues to send all chunks until last chunk, which server /// will send with /// 'complete' set to true /// 4. client starts to receive the data and stream to a temporary file @@ -526,7 +526,7 @@ public enum Com_Apple_Container_Build_V1_BuilderClientMetadata { /// To perform a build: /// /// 1. CreateBuild to create a new build -/// 2. StartBuild to start the build exection where client and server +/// 2. StartBuild to start the build execution where client and server /// both have a stream for exchanging data during the build. /// /// The client may send: @@ -589,7 +589,7 @@ public enum Com_Apple_Container_Build_V1_BuilderClientMetadata { /// source path, and empty data /// 2. server archives the data at source path, and starts to send chunks to /// the client -/// 3. server coninues to send all chunks until last chunk, which server +/// 3. server continues to send all chunks until last chunk, which server /// will send with /// 'complete' set to true /// 4. client starts to receive the data and stream to a temporary file @@ -673,7 +673,7 @@ extension Com_Apple_Container_Build_V1_BuilderProvider { /// To perform a build: /// /// 1. CreateBuild to create a new build -/// 2. StartBuild to start the build exection where client and server +/// 2. StartBuild to start the build execution where client and server /// both have a stream for exchanging data during the build. /// /// The client may send: @@ -736,7 +736,7 @@ extension Com_Apple_Container_Build_V1_BuilderProvider { /// source path, and empty data /// 2. server archives the data at source path, and starts to send chunks to /// the client -/// 3. server coninues to send all chunks until last chunk, which server +/// 3. server continues to send all chunks until last chunk, which server /// will send with /// 'complete' set to true /// 4. client starts to receive the data and stream to a temporary file diff --git a/Sources/ContainerClient/Core/ClientDefaults.swift b/Sources/ContainerClient/Core/ClientDefaults.swift index 8d9a0bcb7..369c295e2 100644 --- a/Sources/ContainerClient/Core/ClientDefaults.swift +++ b/Sources/ContainerClient/Core/ClientDefaults.swift @@ -63,7 +63,8 @@ extension ClientDefaults.Keys { case .defaultKernelBinaryPath: return "opt/kata/share/kata-containers/vmlinux-6.12.28-153" case .defaultBuilderImage: - return "ghcr.io/apple/container-builder-shim/builder:0.1.0" + let tag = String(cString: get_container_builder_shim_version()) + return "ghcr.io/apple/container-builder-shim/builder:\(tag)" case .defaultDNSDomain: return "test" case .defaultRegistryDomain: From 2ac52d4ce0d19310326380d57b0f563a9656bd38 Mon Sep 17 00:00:00 2001 From: Kathryn Baldauf Date: Thu, 12 Jun 2025 11:36:23 -0700 Subject: [PATCH 07/45] Add missing link to repo CoC in issue template (#157) Signed-off-by: Kathryn Baldauf --- .github/ISSUE_TEMPLATE/02-feature.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/02-feature.yml b/.github/ISSUE_TEMPLATE/02-feature.yml index ba1548b59..52cc0a114 100644 --- a/.github/ISSUE_TEMPLATE/02-feature.yml +++ b/.github/ISSUE_TEMPLATE/02-feature.yml @@ -26,7 +26,7 @@ body: id: terms attributes: label: Code of Conduct - description: By submitting this issue, you agree to follow our [Code of Conduct](https://example.com). + description: By submitting this issue, you agree to follow our [Code of Conduct](https://github.com/apple/container/blob/main/CONTRIBUTING.md). options: - label: I agree to follow this project's Code of Conduct required: true From 6f1f770a4ec260dd3a39aa5744ae1db6de6340b4 Mon Sep 17 00:00:00 2001 From: Kathryn Baldauf Date: Thu, 12 Jun 2025 11:36:42 -0700 Subject: [PATCH 08/45] Remove temporary workaround for image auth to ghcr (#155) When we were setting up the repos, we needed these environment variables for the GitHub Actions CI to be able to run the tests. Now that the images are public, these can be removed. Signed-off-by: Kathryn Baldauf --- .github/workflows/common.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.github/workflows/common.yml b/.github/workflows/common.yml index 70cc604b1..67dbf8342 100644 --- a/.github/workflows/common.yml +++ b/.github/workflows/common.yml @@ -61,9 +61,6 @@ jobs: launchctl setenv HTTP_PROXY $HTTP_PROXY make test cleancontent install-kernel integration env: - CONTAINER_REGISTRY_TOKEN: ${{ secrets.GITHUB_TOKEN }} - CONTAINER_REGISTRY_USER: ${{ github.actor }} - CONTAINER_REGISTRY_HOST: ghcr.io DEVELOPER_DIR: "/Applications/Xcode_16.3.app/Contents/Developer" CURRENT_SDK: y - name: Save documentation artifact From 7ca5a4313ff2bc22bb39ad2dd0848ad947953f54 Mon Sep 17 00:00:00 2001 From: Elijah Wright Date: Thu, 12 Jun 2025 11:37:26 -0700 Subject: [PATCH 09/45] define JSONDecoder() outside of for loop in load() (#159) this PR defines a variable, `decoder`, which represents the `JSONDecoder` class, rather than doing it in the for loop in `load()` Signed-off-by: Elijah Wright --- Sources/ContainerPersistence/EntityStore.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Sources/ContainerPersistence/EntityStore.swift b/Sources/ContainerPersistence/EntityStore.swift index ab1b5ff12..7a09665ff 100644 --- a/Sources/ContainerPersistence/EntityStore.swift +++ b/Sources/ContainerPersistence/EntityStore.swift @@ -101,12 +101,13 @@ public actor FilesystemEntityStore: EntityStore where T: Codable & Identifiab private static func load(path: URL, log: Logger) throws -> Index { let directories = try FileManager.default.contentsOfDirectory(at: path, includingPropertiesForKeys: nil) var index: FilesystemEntityStore.Index = Index() + let decoder = JSONDecoder() for entityUrl in directories { do { let metadataUrl = entityUrl.appendingPathComponent(metadataFilename) let data = try Data(contentsOf: metadataUrl) - let entity = try JSONDecoder().decode(T.self, from: data) + let entity = try decoder.decode(T.self, from: data) index[entity.id] = entity } catch { log.warning( From c114945f15e3951756fab2bdf7b02885d29bd8e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=9Cmit=20DEM=C4=B0RC=C4=B0?= <0umitdemirci@gmail.com> Date: Fri, 13 Jun 2025 03:14:15 +0300 Subject: [PATCH 10/45] fix: typo (#153) --- Sources/TerminalProgress/ProgressBar+Terminal.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/TerminalProgress/ProgressBar+Terminal.swift b/Sources/TerminalProgress/ProgressBar+Terminal.swift index e7598f5df..ac6db87e9 100644 --- a/Sources/TerminalProgress/ProgressBar+Terminal.swift +++ b/Sources/TerminalProgress/ProgressBar+Terminal.swift @@ -26,8 +26,8 @@ enum EscapeSequence { extension ProgressBar { private var terminalWidth: Int { guard - let termimalHandle = term, - let terminal = try? Terminal(descriptor: termimalHandle.fileDescriptor) + let terminalHandle = term, + let terminal = try? Terminal(descriptor: terminalHandle.fileDescriptor) else { return 0 } From 9d7593201724bfedc6a2e4b10ef7a2a5c58feb82 Mon Sep 17 00:00:00 2001 From: J Logan Date: Thu, 12 Jun 2025 17:29:07 -0700 Subject: [PATCH 11/45] Adds Swift Package Index crawler metadata. (#181) --- .spi.yml | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 .spi.yml diff --git a/.spi.yml b/.spi.yml new file mode 100644 index 000000000..3899003d2 --- /dev/null +++ b/.spi.yml @@ -0,0 +1,12 @@ +version: 1 +builder: + configs: + - documentation_targets: + - ContainerSandboxService + - ContainerNetworkService + - ContainerImagesService + - ContainerClient + - ContainerLog + - ContainerPlugin + - ContainerXPC + - TerminalProgress From 8f2d4d730040982f78006121b1b348c761888f04 Mon Sep 17 00:00:00 2001 From: Yibo Zhuang Date: Thu, 12 Jun 2025 21:28:24 -0700 Subject: [PATCH 12/45] Fix: consolidate UserDefaults service name (#161) This change consolidates the UserDefaults service name `com.apple.container.defaults` to a single constant under the extension file and also renamed the extension file. Signed-off-by: Yibo Zhuang --- .../ContainerNetworkService/AllocationOnlyVmnetNetwork.swift | 2 +- ...UserDefaults+Backpack.swift => UserDefaults+Container.swift} | 0 Sources/Services/ContainerSandboxService/SandboxService.swift | 2 +- 3 files changed, 2 insertions(+), 2 deletions(-) rename Sources/Services/ContainerNetworkService/{UserDefaults+Backpack.swift => UserDefaults+Container.swift} (100%) diff --git a/Sources/Services/ContainerNetworkService/AllocationOnlyVmnetNetwork.swift b/Sources/Services/ContainerNetworkService/AllocationOnlyVmnetNetwork.swift index 0e74e3545..d7269413c 100644 --- a/Sources/Services/ContainerNetworkService/AllocationOnlyVmnetNetwork.swift +++ b/Sources/Services/ContainerNetworkService/AllocationOnlyVmnetNetwork.swift @@ -64,7 +64,7 @@ public actor AllocationOnlyVmnetNetwork: Network { ] ) - if let suite = UserDefaults.init(suiteName: "com.apple.container.defaults") { + if let suite = UserDefaults.init(suiteName: UserDefaults.appSuiteName) { // TODO: Make the suiteName a constant defined in ClientDefaults and use that. // This will need some re-working of dependencies between NetworkService and Client defaultSubnet = suite.string(forKey: "network.subnet") ?? defaultSubnet diff --git a/Sources/Services/ContainerNetworkService/UserDefaults+Backpack.swift b/Sources/Services/ContainerNetworkService/UserDefaults+Container.swift similarity index 100% rename from Sources/Services/ContainerNetworkService/UserDefaults+Backpack.swift rename to Sources/Services/ContainerNetworkService/UserDefaults+Container.swift diff --git a/Sources/Services/ContainerSandboxService/SandboxService.swift b/Sources/Services/ContainerSandboxService/SandboxService.swift index 9d89ca380..17c1b3f2c 100644 --- a/Sources/Services/ContainerSandboxService/SandboxService.swift +++ b/Sources/Services/ContainerSandboxService/SandboxService.swift @@ -93,7 +93,7 @@ public actor SandboxService { let fqdn: String if let hostname = config.hostname { - if let suite = UserDefaults.init(suiteName: "com.apple.container.defaults"), + if let suite = UserDefaults.init(suiteName: UserDefaults.appSuiteName), let dnsDomain = suite.string(forKey: "dns.domain"), !hostname.contains(".") { From 206f3cc07c6fd1272066fc29fc5fa1a71ba46ca6 Mon Sep 17 00:00:00 2001 From: Dmitry Kovba Date: Thu, 12 Jun 2025 23:36:38 -0700 Subject: [PATCH 13/45] Improve accuracy of progress updates (#144) This PR resolves the problem of dropped progress updates and ensures the accuracy of the information provided in the progress bar. Additionally, it adds the displaying of the finished state to the progress bar. Requires merging and tagging https://github.com/apple/containerization/pull/91. --- Package.resolved | 6 +- Package.swift | 2 +- Sources/ContainerXPC/XPCClient.swift | 1 - .../TerminalProgress/ProgressBar+Add.swift | 99 ++++++--- Sources/TerminalProgress/ProgressBar.swift | 95 +++++--- Sources/TerminalProgress/ProgressTheme.swift | 5 +- .../ProgressBarTests.swift | 203 ++++++++++++++++++ 7 files changed, 336 insertions(+), 75 deletions(-) diff --git a/Package.resolved b/Package.resolved index 5fb976ba9..c6259c6cf 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "cb5cf6c245fe60f647e4e106917dc2e6559eda5170af37e63e52e549b2eed566", + "originHash" : "21dad499f34492edb861e54fe1e03ee3b00aa3d7371af6b09253bf04495427b9", "pins" : [ { "identity" : "async-http-client", @@ -15,8 +15,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/containerization.git", "state" : { - "revision" : "56c06be9a9473b6df9031dd217551e6ad9a97d29", - "version" : "0.1.0" + "revision" : "4b05e5f2313e881ee048f7063b30e73070fbd1b1", + "version" : "0.1.1" } }, { diff --git a/Package.swift b/Package.swift index 1ca4e9d1a..084d16b73 100644 --- a/Package.swift +++ b/Package.swift @@ -26,7 +26,7 @@ if let path = ProcessInfo.processInfo.environment["CONTAINERIZATION_PATH"] { scDependency = .package(path: path) scVersion = "latest" } else { - scVersion = "0.1.0" + scVersion = "0.1.1" scDependency = .package(url: "https://github.com/apple/containerization.git", exact: Version(stringLiteral: scVersion)) } diff --git a/Sources/ContainerXPC/XPCClient.swift b/Sources/ContainerXPC/XPCClient.swift index 16bea4682..dbe07bce4 100644 --- a/Sources/ContainerXPC/XPCClient.swift +++ b/Sources/ContainerXPC/XPCClient.swift @@ -19,7 +19,6 @@ import ContainerizationError import Foundation public struct XPCClient: Sendable { - // Access to `connection` is protected by a lock private nonisolated(unsafe) let connection: xpc_connection_t private let q: DispatchQueue? private let service: String diff --git a/Sources/TerminalProgress/ProgressBar+Add.swift b/Sources/TerminalProgress/ProgressBar+Add.swift index 953b308dd..7f5a68f94 100644 --- a/Sources/TerminalProgress/ProgressBar+Add.swift +++ b/Sources/TerminalProgress/ProgressBar+Add.swift @@ -61,23 +61,24 @@ extension ProgressBar { /// Performs a check to see if the progress bar should be finished. public func checkIfFinished() { - if let totalTasks = state.totalTasks { + var finished = true + var defined = false + if let totalTasks = state.totalTasks, totalTasks > 0 { // For tasks, we're showing the current task rather then the number of completed tasks. - guard state.tasks > totalTasks else { - return - } + finished = finished && state.tasks == totalTasks + defined = true } - if let totalItems = state.totalItems { - guard state.items == totalItems else { - return - } + if let totalItems = state.totalItems, totalItems > 0 { + finished = finished && state.items == totalItems + defined = true } - if let totalSize = state.totalSize { - guard state.size == totalSize else { - return - } + if let totalSize = state.totalSize, totalSize > 0 { + finished = finished && state.size == totalSize + defined = true + } + if defined && finished { + finish() } - finish() } /// Sets the current tasks. @@ -92,9 +93,14 @@ extension ProgressBar { /// Performs an addition to the current tasks. /// - Parameter tasks: The tasks to add to the current tasks. - public func add(tasks toAdd: Int, render: Bool = true) { - let newTasks = state.tasks + toAdd - set(tasks: newTasks, render: render) + public func add(tasks delta: Int, render: Bool = true) { + _state.withLock { + let newTasks = $0.tasks + delta + $0.tasks = newTasks + } + if render { + self.render() + } } /// Sets the total tasks. @@ -108,10 +114,15 @@ extension ProgressBar { /// Performs an addition to the total tasks. /// - Parameter totalTasks: The tasks to add to the total tasks. - public func add(totalTasks toAdd: Int, render: Bool = true) { - let totalTasks = state.totalTasks ?? 0 - let newTotalTasks = totalTasks + toAdd - set(totalTasks: newTotalTasks, render: render) + public func add(totalTasks delta: Int, render: Bool = true) { + _state.withLock { + let totalTasks = $0.totalTasks ?? 0 + let newTotalTasks = totalTasks + delta + $0.totalTasks = newTotalTasks + } + if render { + self.render() + } } /// Sets the items name. @@ -134,9 +145,14 @@ extension ProgressBar { /// Performs an addition to the current items. /// - Parameter items: The items to add to the current items. - public func add(items toAdd: Int, render: Bool = true) { - let newItems = state.items + toAdd - set(items: newItems, render: render) + public func add(items delta: Int, render: Bool = true) { + _state.withLock { + let newItems = $0.items + delta + $0.items = newItems + } + if render { + self.render() + } } /// Sets the total items. @@ -150,10 +166,15 @@ extension ProgressBar { /// Performs an addition to the total items. /// - Parameter totalItems: The items to add to the total items. - public func add(totalItems toAdd: Int, render: Bool = true) { - let totalItems = state.totalItems ?? 0 - let newTotalItems = totalItems + toAdd - set(totalItems: newTotalItems, render: render) + public func add(totalItems delta: Int, render: Bool = true) { + _state.withLock { + let totalItems = $0.totalItems ?? 0 + let newTotalItems = totalItems + delta + $0.totalItems = newTotalItems + } + if render { + self.render() + } } /// Sets the current size. @@ -167,9 +188,14 @@ extension ProgressBar { /// Performs an addition to the current size. /// - Parameter size: The size to add to the current size. - public func add(size toAdd: Int64, render: Bool = true) { - let newSize = state.size + toAdd - set(size: newSize, render: render) + public func add(size delta: Int64, render: Bool = true) { + _state.withLock { + let newSize = $0.size + delta + $0.size = newSize + } + if render { + self.render() + } } /// Sets the total size. @@ -183,9 +209,14 @@ extension ProgressBar { /// Performs an addition to the total size. /// - Parameter totalSize: The size to add to the total size. - public func add(totalSize toAdd: Int64, render: Bool = true) { - let totalSize = state.totalSize ?? 0 - let newTotalSize = totalSize + toAdd - set(totalSize: newTotalSize, render: render) + public func add(totalSize delta: Int64, render: Bool = true) { + _state.withLock { + let totalSize = $0.totalSize ?? 0 + let newTotalSize = totalSize + delta + $0.totalSize = newTotalSize + } + if render { + self.render() + } } } diff --git a/Sources/TerminalProgress/ProgressBar.swift b/Sources/TerminalProgress/ProgressBar.swift index c28106915..a49529655 100644 --- a/Sources/TerminalProgress/ProgressBar.swift +++ b/Sources/TerminalProgress/ProgressBar.swift @@ -20,8 +20,9 @@ import SendableProperty /// A progress bar that updates itself as tasks are completed. public final class ProgressBar: Sendable { let config: ProgressConfig + // `@SendableProperty` adds `_state: Synchronized`, which can be updated inside a lock using `_state.withLock()`. @SendableProperty - var state: State + var state = State() @SendableProperty var printedWidth = 0 let term: FileHandle? @@ -97,7 +98,7 @@ public final class ProgressBar: Sendable { printFullDescription() } - while !isFinished { + while !state.finished { let intervalNanoseconds = UInt64(intervalSeconds * 1_000_000_000) render() state.iteration += 1 @@ -117,11 +118,15 @@ public final class ProgressBar: Sendable { /// Finishes the progress bar. public func finish() { - guard !isFinished else { + guard !state.finished else { return } state.finished = true + + // The last render. + render(force: true) + if !config.disableProgressUpdates && !config.clearOnFinish { displayText(state.output, terminating: "\n") } @@ -143,8 +148,8 @@ extension ProgressBar { return timeDifferenceSeconds } - func render() { - guard term != nil && !config.disableProgressUpdates && !isFinished else { + func render(force: Bool = false) { + guard term != nil && !config.disableProgressUpdates && (force || !state.finished) else { return } let output = draw() @@ -154,8 +159,12 @@ extension ProgressBar { func draw() -> String { var components = [String]() if config.showSpinner && !config.showProgressBar { - let spinnerIcon = config.theme.getSpinnerIcon(state.iteration) - components.append("\(spinnerIcon)") + if !state.finished { + let spinnerIcon = config.theme.getSpinnerIcon(state.iteration) + components.append("\(spinnerIcon)") + } else { + components.append("\(config.theme.done)") + } } if config.showTasks, let totalTasks = state.totalTasks { @@ -176,13 +185,13 @@ extension ProgressBar { let total = state.totalSize ?? Int64(state.totalItems ?? 0) if config.showPercent && total > 0 && allowProgress { - components.append("\(state.percent)") + components.append("\(state.finished ? "100%" : state.percent)") } if config.showProgressBar, total > 0, allowProgress { let usedWidth = components.joined(separator: " ").count + 45 /* the maximum number of characters we may need */ let remainingWidth = max(config.width - usedWidth, 1 /* the minumum width of a progress bar */) - let barLength = Int(Int64(remainingWidth) * value / total) + let barLength = state.finished ? remainingWidth : Int(Int64(remainingWidth) * value / total) let barPaddingLength = remainingWidth - barLength let bar = "\(String(repeating: config.theme.bar, count: barLength))\(String(repeating: " ", count: barPaddingLength))" components.append("|\(bar)|") @@ -195,40 +204,56 @@ extension ProgressBar { if !state.itemsName.isEmpty { itemsName = " \(state.itemsName)" } - if let totalItems = state.totalItems { - additionalComponents.append("\(state.items.formattedNumber()) of \(totalItems.formattedNumber())\(itemsName)") + if state.finished { + if let totalItems = state.totalItems { + additionalComponents.append("\(totalItems.formattedNumber())\(itemsName)") + } } else { - additionalComponents.append("\(state.items.formattedNumber())\(itemsName)") + if let totalItems = state.totalItems { + additionalComponents.append("\(state.items.formattedNumber()) of \(totalItems.formattedNumber())\(itemsName)") + } else { + additionalComponents.append("\(state.items.formattedNumber())\(itemsName)") + } } } if state.size > 0 && allowProgress { - var formattedCombinedSize = "" - if config.showSize { - var formattedSize = state.size.formattedSize() - formattedSize = adjustFormattedSize(formattedSize) - if let totalSize = state.totalSize { - var formattedTotalSize = totalSize.formattedSize() - formattedTotalSize = adjustFormattedSize(formattedTotalSize) - formattedCombinedSize = combineSize(size: formattedSize, totalSize: formattedTotalSize) - } else { - formattedCombinedSize = formattedSize + if state.finished { + if config.showSize { + if let totalSize = state.totalSize { + var formattedTotalSize = totalSize.formattedSize() + formattedTotalSize = adjustFormattedSize(formattedTotalSize) + additionalComponents.append(formattedTotalSize) + } + } + } else { + var formattedCombinedSize = "" + if config.showSize { + var formattedSize = state.size.formattedSize() + formattedSize = adjustFormattedSize(formattedSize) + if let totalSize = state.totalSize { + var formattedTotalSize = totalSize.formattedSize() + formattedTotalSize = adjustFormattedSize(formattedTotalSize) + formattedCombinedSize = combineSize(size: formattedSize, totalSize: formattedTotalSize) + } else { + formattedCombinedSize = formattedSize + } } - } - var formattedSpeed = "" - if config.showSpeed { - formattedSpeed = "\(state.sizeSpeed ?? state.averageSizeSpeed)" - formattedSpeed = adjustFormattedSize(formattedSpeed) - } + var formattedSpeed = "" + if config.showSpeed { + formattedSpeed = "\(state.sizeSpeed ?? state.averageSizeSpeed)" + formattedSpeed = adjustFormattedSize(formattedSpeed) + } - if config.showSize && config.showSpeed { - additionalComponents.append(formattedCombinedSize) - additionalComponents.append(formattedSpeed) - } else if config.showSize { - additionalComponents.append(formattedCombinedSize) - } else if config.showSpeed { - additionalComponents.append(formattedSpeed) + if config.showSize && config.showSpeed { + additionalComponents.append(formattedCombinedSize) + additionalComponents.append(formattedSpeed) + } else if config.showSize { + additionalComponents.append(formattedCombinedSize) + } else if config.showSpeed { + additionalComponents.append(formattedSpeed) + } } } diff --git a/Sources/TerminalProgress/ProgressTheme.swift b/Sources/TerminalProgress/ProgressTheme.swift index fc537d405..96577261f 100644 --- a/Sources/TerminalProgress/ProgressTheme.swift +++ b/Sources/TerminalProgress/ProgressTheme.swift @@ -18,13 +18,16 @@ public protocol ProgressTheme: Sendable { /// The icons used to represent a spinner. var spinner: [String] { get } - /// The icons used to represent a progress bar. + /// The icon used to represent a progress bar. var bar: String { get } + /// The icon used to indicate that a progress bar finished. + var done: String { get } } public struct DefaultProgressTheme: ProgressTheme { public let spinner = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"] public let bar = "█" + public let done = "✔" } extension ProgressTheme { diff --git a/Tests/TerminalProgressTests/ProgressBarTests.swift b/Tests/TerminalProgressTests/ProgressBarTests.swift index a1eb9b3ff..2b432e033 100644 --- a/Tests/TerminalProgressTests/ProgressBarTests.swift +++ b/Tests/TerminalProgressTests/ProgressBarTests.swift @@ -30,6 +30,16 @@ final class ProgressBarTests: XCTestCase { XCTAssertEqual(output, "⠋ Task [0s]") } + func testSpinnerFinished() async throws { + let config = try ProgressConfig( + description: "Task" + ) + let progress = ProgressBar(config: config) + progress.finish() + let output = progress.draw() + XCTAssertEqual(output, "✔ Task [0s]") + } + func testNoSpinner() async throws { let config = try ProgressConfig( description: "Task", @@ -40,6 +50,17 @@ final class ProgressBarTests: XCTestCase { XCTAssertEqual(output, "Task [0s]") } + func testNoSpinnerFinished() async throws { + let config = try ProgressConfig( + description: "Task", + showSpinner: false + ) + let progress = ProgressBar(config: config) + progress.finish() + let output = progress.draw() + XCTAssertEqual(output, "Task [0s]") + } + func testNoTasks() async throws { let config = try ProgressConfig( description: "Task", @@ -93,6 +114,18 @@ final class ProgressBarTests: XCTestCase { XCTAssertEqual(output, "⠋ [0/2] Task [0s]") } + func testTotalTasksFinished() async throws { + let config = try ProgressConfig( + description: "Task", + showTasks: true, + totalTasks: 2 + ) + let progress = ProgressBar(config: config) + progress.finish() + let output = progress.draw() + XCTAssertEqual(output, "✔ [0/2] Task [0s]") + } + func testTotalTasksAdd() async throws { let config = try ProgressConfig( description: "Task", @@ -176,6 +209,19 @@ final class ProgressBarTests: XCTestCase { XCTAssertEqual(output, "⠋ Task 50% [0s]") } + func testPercentItemsFinished() async throws { + let config = try ProgressConfig( + description: "Task", + showPercent: true, + totalItems: 2 + ) + let progress = ProgressBar(config: config) + progress.set(items: 1) + progress.finish() + let output = progress.draw() + XCTAssertEqual(output, "✔ Task 100% [0s]") + } + func testPercentSize() async throws { let config = try ProgressConfig( description: "Task", @@ -190,6 +236,21 @@ final class ProgressBarTests: XCTestCase { XCTAssertEqual(output, "⠋ Task 50% [0s]") } + func testPercentSizeFinished() async throws { + let config = try ProgressConfig( + description: "Task", + showPercent: true, + showSize: false, + showSpeed: false, + totalSize: 2 + ) + let progress = ProgressBar(config: config) + progress.set(size: 1) + progress.finish() + let output = progress.draw() + XCTAssertEqual(output, "✔ Task 100% [0s]") + } + func testNoProgressBar() async throws { let config = try ProgressConfig( description: "Task", @@ -216,6 +277,20 @@ final class ProgressBarTests: XCTestCase { XCTAssertEqual(output, "Task 50% |██ | [0s]") } + func testProgressBarFinished() async throws { + let config = try ProgressConfig( + description: "Task", + showProgressBar: true, + totalItems: 2, + width: 57 + ) + let progress = ProgressBar(config: config) + progress.set(items: 1) + progress.finish() + let output = progress.draw() + XCTAssertEqual(output, "Task 100% |███| [0s]") + } + func testProgressBarMinWidth() async throws { let config = try ProgressConfig( description: "Task", @@ -229,6 +304,20 @@ final class ProgressBarTests: XCTestCase { XCTAssertEqual(output, "Task 50% | | [0s]") } + func testProgressBarMinWidthFinished() async throws { + let config = try ProgressConfig( + description: "Task", + showProgressBar: true, + totalItems: 2, + width: 13 + ) + let progress = ProgressBar(config: config) + progress.set(items: 1) + progress.finish() + let output = progress.draw() + XCTAssertEqual(output, "Task 100% |█| [0s]") + } + func testNoItems() async throws { let config = try ProgressConfig( description: "Task", @@ -260,6 +349,18 @@ final class ProgressBarTests: XCTestCase { XCTAssertEqual(output, "⠋ Task (1 it) [0s]") } + func testItemsAddFinish() async throws { + let config = try ProgressConfig( + description: "Task", + showItems: true + ) + let progress = ProgressBar(config: config) + progress.add(items: 1) + progress.finish() + let output = progress.draw() + XCTAssertEqual(output, "✔ Task [0s]") + } + func testItemsSet() async throws { let config = try ProgressConfig( description: "Task", @@ -294,6 +395,19 @@ final class ProgressBarTests: XCTestCase { XCTAssertEqual(output, "⠋ Task 50% (1 of 2 it) [0s]") } + func testTotalItemsFinish() async throws { + let config = try ProgressConfig( + description: "Task", + showItems: true, + totalItems: 2 + ) + let progress = ProgressBar(config: config) + progress.set(items: 1) + progress.finish() + let output = progress.draw() + XCTAssertEqual(output, "✔ Task 100% (2 it) [0s]") + } + func testTotalItemsAdd() async throws { let config = try ProgressConfig( description: "Task", @@ -361,6 +475,19 @@ final class ProgressBarTests: XCTestCase { XCTAssertEqual(output, "⠋ Task (1 byte) [0s]") } + func testSizeAddFinish() async throws { + let config = try ProgressConfig( + description: "Task", + showSize: true, + showSpeed: false + ) + let progress = ProgressBar(config: config) + progress.add(size: 1) + progress.finish() + let output = progress.draw() + XCTAssertEqual(output, "✔ Task [0s]") + } + func testSizeSet() async throws { let config = try ProgressConfig( description: "Task", @@ -397,6 +524,20 @@ final class ProgressBarTests: XCTestCase { XCTAssertEqual(output, "⠋ Task 50% (1 byte/2 bytes) [0s]") } + func testTotalSizeDifferentUnitsFinish() async throws { + let config = try ProgressConfig( + description: "Task", + showSize: true, + showSpeed: false, + totalSize: 2 + ) + let progress = ProgressBar(config: config) + progress.set(size: 1) + progress.finish() + let output = progress.draw() + XCTAssertEqual(output, "✔ Task 100% (2 bytes) [0s]") + } + func testTotalSizeSameUnits() async throws { let config = try ProgressConfig( description: "Task", @@ -410,6 +551,20 @@ final class ProgressBarTests: XCTestCase { XCTAssertEqual(output, "⠋ Task 50% (2/4 bytes) [0s]") } + func testTotalSizeSameUnitsFinish() async throws { + let config = try ProgressConfig( + description: "Task", + showSize: true, + showSpeed: false, + totalSize: 4 + ) + let progress = ProgressBar(config: config) + progress.set(size: 2) + progress.finish() + let output = progress.draw() + XCTAssertEqual(output, "✔ Task 100% (4 bytes) [0s]") + } + func testTotalSizeAdd() async throws { let config = try ProgressConfig( description: "Task", @@ -463,6 +618,23 @@ final class ProgressBarTests: XCTestCase { XCTAssertEqual(output, "⠋ Task 50% (1 of 2 it, 2/4 bytes) [0s]") } + func testItemsAndSizeFinish() async throws { + let config = try ProgressConfig( + description: "Task", + showItems: true, + showSize: true, + showSpeed: false, + totalItems: 2, + totalSize: 4 + ) + let progress = ProgressBar(config: config) + progress.set(items: 1) + progress.set(size: 2) + progress.finish() + let output = progress.draw() + XCTAssertEqual(output, "✔ Task 100% (2 it, 4 bytes) [0s]") + } + func testNoSpeed() async throws { let config = try ProgressConfig( description: "Task", @@ -487,6 +659,19 @@ final class ProgressBarTests: XCTestCase { XCTAssertTrue(output.contains("/s")) } + func testSpeedFinish() async throws { + let config = try ProgressConfig( + description: "Task", + showSpeed: true, + totalSize: 4 + ) + let progress = ProgressBar(config: config) + progress.set(size: 2) + progress.finish() + let output = progress.draw() + XCTAssertFalse(output.contains("/s")) + } + func testItemsSizeAndSpeed() async throws { let config = try ProgressConfig( description: "Task", @@ -504,6 +689,24 @@ final class ProgressBarTests: XCTestCase { XCTAssertTrue(output.contains("/s")) } + func testItemsSizeAndSpeedFinish() async throws { + let config = try ProgressConfig( + description: "Task", + showItems: true, + showSize: true, + showSpeed: true, + totalItems: 2, + totalSize: 4 + ) + let progress = ProgressBar(config: config) + progress.set(items: 1) + progress.set(size: 2) + progress.finish() + let output = progress.draw() + XCTAssertTrue(output.contains("2 it, 4 bytes")) + XCTAssertFalse(output.contains("/s")) + } + func testNoTime() async throws { let config = try ProgressConfig( description: "Task", From 2a12f01d3201eca76efa6e6cdbfea867dc490258 Mon Sep 17 00:00:00 2001 From: Alexey Makhov Date: Fri, 13 Jun 2025 17:33:05 +0300 Subject: [PATCH 14/45] container registry login host:port error fix (#170) Fixes #121 Gets port from the given registry server URL and use it in a RegistryClient. Signed-off-by: Alexey Makhov --- Sources/CLI/Registry/Login.swift | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/Sources/CLI/Registry/Login.swift b/Sources/CLI/Registry/Login.swift index 617bc801e..7de7fe7e4 100644 --- a/Sources/CLI/Registry/Login.swift +++ b/Sources/CLI/Registry/Login.swift @@ -63,10 +63,18 @@ extension Application { let server = Reference.resolveDomain(domain: server) let scheme = try RequestScheme(registry.scheme).schemeFor(host: server) + let _url = "\(scheme)://\(server)" + guard let url = URL(string: _url) else { + throw ContainerizationError(.invalidArgument, message: "Cannot convert \(_url) to URL") + } + guard let host = url.host else { + throw ContainerizationError(.invalidArgument, message: "Invalid host \(server)") + } let client = RegistryClient( - host: server, + host: host, scheme: scheme.rawValue, + port: url.port, authentication: BasicAuthentication(username: username, password: password), retryOptions: .init( maxRetries: 10, From c0433bec1565118051913e3ea5ab282a05f30d0a Mon Sep 17 00:00:00 2001 From: Dmitry Kovba Date: Fri, 13 Jun 2025 09:11:32 -0700 Subject: [PATCH 15/45] Update issue templates (#184) Please see commits for the list of changes. --- .github/ISSUE_TEMPLATE/01-bug.yml | 25 ++++++++----------------- .github/ISSUE_TEMPLATE/02-feature.yml | 12 ++---------- .github/ISSUE_TEMPLATE/config.yml | 4 ++-- 3 files changed, 12 insertions(+), 29 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/01-bug.yml b/.github/ISSUE_TEMPLATE/01-bug.yml index a194fc4b6..2657edfa6 100644 --- a/.github/ISSUE_TEMPLATE/01-bug.yml +++ b/.github/ISSUE_TEMPLATE/01-bug.yml @@ -1,4 +1,4 @@ -name: Bug Report +name: Bug report description: File a bug report. title: "[Bug]: " labels: ["bug", "triage"] @@ -17,13 +17,6 @@ body: required: true - label: If possible, I've reproduced the issue using the 'main' branch of this project required: false - - type: input - id: contact - attributes: - label: Contact Details - description: How can we get in touch with you if we need more info? ex. email@example.com - validations: - required: false - type: textarea id: reproduce attributes: @@ -49,16 +42,14 @@ body: attributes: label: Environment description: | - examples: - - **OS**: MacOS 26 Beta 1 - - **swift**: Apple Swift version 6.2 - - **xcode**: Xcode 26 Beta 17A5241e - - **container**: container CLI version 0.1.0 + Examples: + - **OS**: macOS 26.0 Beta (25A5279m) + - **Xcode**: Version 26.0 beta (17A5241e) + - **Container**: Container CLI version 0.1.0 value: | - OS: - - swift: - - xcode: - - container: + - Xcode: + - Container: render: markdown validations: required: true @@ -72,7 +63,7 @@ body: id: terms attributes: label: Code of Conduct - description: By submitting this issue, you agree to follow our [Code of Conduct](https://github.com/apple/container/blob/main/CONTRIBUTING.md). + description: By submitting this issue, you agree to follow our [Code of Conduct](https://github.com/apple/.github/blob/main/CODE_OF_CONDUCT.md). options: - label: I agree to follow this project's Code of Conduct required: true diff --git a/.github/ISSUE_TEMPLATE/02-feature.yml b/.github/ISSUE_TEMPLATE/02-feature.yml index 52cc0a114..d40fca4da 100644 --- a/.github/ISSUE_TEMPLATE/02-feature.yml +++ b/.github/ISSUE_TEMPLATE/02-feature.yml @@ -1,4 +1,4 @@ -name: Feature or Enhancement request +name: Feature or enhancement request description: File a request for a feature or enhancement title: "[Request]: " labels: ["feature", "triage"] @@ -7,14 +7,6 @@ body: attributes: value: | Thanks for contributing to the container project! - - type: input - id: contact - attributes: - label: Contact Details - description: How can we get in touch with you if we need more info? - placeholder: ex. email@example.com - validations: - required: false - type: textarea id: request attributes: @@ -26,7 +18,7 @@ body: id: terms attributes: label: Code of Conduct - description: By submitting this issue, you agree to follow our [Code of Conduct](https://github.com/apple/container/blob/main/CONTRIBUTING.md). + description: By submitting this issue, you agree to follow our [Code of Conduct](https://github.com/apple/.github/blob/main/CODE_OF_CONDUCT.md). options: - label: I agree to follow this project's Code of Conduct required: true diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 2dff99170..4a3a318ea 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,5 +1,5 @@ blank_issues_enabled: false contact_links: - - name: container community support + - name: Container community support url: https://github.com/apple/container/discussions - about: Please ask and answer questions here. \ No newline at end of file + about: Please ask and answer questions here. From 878ebbda2e0f6792ca20f7802250cc607f0b5632 Mon Sep 17 00:00:00 2001 From: Kathryn Baldauf Date: Fri, 13 Jun 2025 09:50:05 -0700 Subject: [PATCH 16/45] Add default year for hawkeye formatting (#180) This adds a default year to use when a file does not yet have git attributes (aka for a newly created file). Signed-off-by: Kathryn Baldauf --- Package.swift | 2 +- scripts/install-hawkeye.sh | 2 +- scripts/license-header.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Package.swift b/Package.swift index 084d16b73..288ed99ba 100644 --- a/Package.swift +++ b/Package.swift @@ -310,7 +310,7 @@ let package = Package( .define("CZ_VERSION", to: "\"\(scVersion)\""), .define("GIT_COMMIT", to: "\"\(gitCommit)\""), .define("RELEASE_VERSION", to: "\"\(releaseVersion)\""), - .define("BUILDER_SHIM_VERSION", to: "\"\(builderShimVersion)\"") + .define("BUILDER_SHIM_VERSION", to: "\"\(builderShimVersion)\""), ] ), ] diff --git a/scripts/install-hawkeye.sh b/scripts/install-hawkeye.sh index 79aaef524..dbe4b9a66 100755 --- a/scripts/install-hawkeye.sh +++ b/scripts/install-hawkeye.sh @@ -17,6 +17,6 @@ if command -v .local/bin/hawkeye >/dev/null 2>&1; then echo "hawkeye already installed" else echo "Installing hawkeye" - export VERSION=v6.0.4 + export VERSION=v6.1.0 curl --proto '=https' --tlsv1.2 -LsSf https://github.com/korandoru/hawkeye/releases/download/${VERSION}/hawkeye-installer.sh | CARGO_HOME=.local sh -s -- --no-modify-path fi diff --git a/scripts/license-header.txt b/scripts/license-header.txt index b26825ef0..1db06c349 100644 --- a/scripts/license-header.txt +++ b/scripts/license-header.txt @@ -1,4 +1,4 @@ -Copyright ©{{ " " }}{%- if attrs.git_file_modified_year != attrs.git_file_created_year -%}{{ attrs.git_file_created_year }}-{{ attrs.git_file_modified_year }}{%- else -%}{{ attrs.git_file_created_year }}{%- endif -%}{{ " " }}{{ props["copyrightOwner"] }}. All rights reserved. +Copyright ©{{ " " }}{%- set created = attrs.git_file_created_year or attrs.disk_file_created_year -%}{%- set modified = attrs.git_file_modified_year or created -%}{%- if created != modified -%} {{created}}-{{modified}}{%- else -%}{{created}}{%- endif -%}{{ " " }}{{ props["copyrightOwner"] }}. All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. From 2364f33cc6ac9a5df3b3745f991e9060c74ea6e5 Mon Sep 17 00:00:00 2001 From: Satyam Singh <68936323+Thedarkmatter10@users.noreply.github.com> Date: Fri, 13 Jun 2025 23:13:59 +0530 Subject: [PATCH 17/45] Fix release workflow: tag regex, artifact validation, and token usage (#187) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 🔧 Summary This PR improves the release.yml GitHub Actions workflow by addressing several critical issues to ensure consistent and reliable behavior during tag-based releases. ## ✅ Changes Included ### 1. Fixed tag trigger regex - Escaped dots in the tag regex ([0-9]+\\.[0-9]+\\.[0-9]+) to ensure only semantic version tags like 1.2.3 trigger the workflow. ##### Explanation - Old **Regex** is incorrect because . matches **any character**, so it matched: `1-2-3`, `1_2_3`, even `1a2b3`, which is invalid for versioning. - Fixed **Regex** is strict — matches only 1.2.3. #### Find attached tested SCREEN SHOT BELOW . **Bad Match with Old Regex:** ![proofwithBadMatch](https://github.com/user-attachments/assets/6dbec50d-6ffb-4338-a258-7b3629b08e24) **Proper Match with Fixed Regex:** ![withfixedbadstring](https://github.com/user-attachments/assets/1c9b6315-074b-4db4-a911-6493a76b4b64) --- ### 💬 Note: If you're planning to adopt alternate version tag formats in the future — such as: - v1.0.2 (semantic with prefix) - release-1.0.2 or rel-1.0.2 - 1.0.2-beta, 1.0.2-rc.1 (prereleases with suffixes) - x1.0.2x (custom wrapping formats) **…feel free to reach out. I'm happy to help extend the workflow to support those formats reliably and safely.** --- ### 2. Added strict release job guard - Prevented **accidental release runs** on non-tag events using if: startsWith(github.ref, 'refs/tags/'). ### 3. Explicit artifact validation - **Introduced a shell check** using ls and test to ensure .zip and .pkg files exist before attempting release. This gives early, clear failure instead of a **vague error** from `action-gh-release`. ### 4. Clarified GitHub token usage - Switched from ${{ secrets.GITHUB_TOKEN }} to ${{ github.token }} for better readability and consistency with GitHub Actions best practices. @katiewasnothere @wlan0 --- .github/workflows/release.yml | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 98e7a9cbc..7edb8cc95 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -3,7 +3,7 @@ name: container project - release build on: push: tags: - - "[0-9]+.[0-9]+.[0-9]+" + - "[0-9]+\\.[0-9]+\\.[0-9]+" jobs: build: @@ -16,7 +16,9 @@ jobs: contents: read packages: read pages: write + release: + if: startsWith(github.ref, 'refs/tags/') name: Publish release timeout-minutes: 30 needs: build @@ -30,10 +32,18 @@ jobs: uses: actions/download-artifact@v4 with: path: outputs + + - name: Verify artifacts exist + run: | + echo "Checking for expected artifacts..." + ls -la outputs/container-package/ + test -e outputs/container-package/*.zip || (echo "Missing .zip file!" && exit 1) + test -e outputs/container-package/*.pkg || (echo "Missing .pkg file!" && exit 1) + - name: Create release uses: softprops/action-gh-release@v2 with: - token: ${{ secrets.GITHUB_TOKEN }} + token: ${{ github.token }} name: ${{ github.ref_name }}-prerelease draft: true make_latest: false From 84edaa20dbc3dde400640ca8e72e568af279b0c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20St=C3=B6ckle?= Date: Fri, 13 Jun 2025 20:07:07 +0200 Subject: [PATCH 18/45] Fix typos (#122) Hi there, some small typos I fixed using [`typos`](https://github.com/crate-ci/typos). Cheers, Patrick --- Sources/CLI/Image/ImageList.swift | 4 ++-- Sources/ContainerClient/XPC+.swift | 2 +- Sources/ContainerXPC/XPCMessage.swift | 2 +- Sources/Services/ContainerSandboxService/SandboxService.swift | 2 +- Sources/TerminalProgress/ProgressBar.swift | 2 +- Sources/TerminalProgress/ProgressConfig.swift | 4 ++-- Tests/ContainerBuildTests/GlobberTests.swift | 2 +- 7 files changed, 9 insertions(+), 9 deletions(-) diff --git a/Sources/CLI/Image/ImageList.swift b/Sources/CLI/Image/ImageList.swift index 8b7bec570..e666feca7 100644 --- a/Sources/CLI/Image/ImageList.swift +++ b/Sources/CLI/Image/ImageList.swift @@ -144,8 +144,8 @@ extension Application { if options.quiet && options.verbose { throw ContainerizationError(.invalidArgument, message: "Cannot use flag --quite and --verbose together") } - let modifer = options.quiet || options.verbose - if modifer && options.format == .json { + let modifier = options.quiet || options.verbose + if modifier && options.format == .json { throw ContainerizationError(.invalidArgument, message: "Cannot use flag --quite or --verbose along with --format json") } } diff --git a/Sources/ContainerClient/XPC+.swift b/Sources/ContainerClient/XPC+.swift index cbf977d4d..183a239f1 100644 --- a/Sources/ContainerClient/XPC+.swift +++ b/Sources/ContainerClient/XPC+.swift @@ -36,7 +36,7 @@ public enum XPCKeys: String { case port /// Exit code for a process case exitCode - /// An event that occured in a container + /// An event that occurred in a container case containerEvent /// Error key. case error diff --git a/Sources/ContainerXPC/XPCMessage.swift b/Sources/ContainerXPC/XPCMessage.swift index 5b81cabdf..eb1f67373 100644 --- a/Sources/ContainerXPC/XPCMessage.swift +++ b/Sources/ContainerXPC/XPCMessage.swift @@ -18,7 +18,7 @@ import ContainerizationError import Foundation -/// A message that can be pass across application boundries via XPC. +/// A message that can be pass across application boundaries via XPC. public struct XPCMessage: Sendable { /// Defined message key storing the route value. public static let routeKey = "com.apple.container.xpc.route" diff --git a/Sources/Services/ContainerSandboxService/SandboxService.swift b/Sources/Services/ContainerSandboxService/SandboxService.swift index 17c1b3f2c..6e901e715 100644 --- a/Sources/Services/ContainerSandboxService/SandboxService.swift +++ b/Sources/Services/ContainerSandboxService/SandboxService.swift @@ -399,7 +399,7 @@ public actor SandboxService { return message.reply() } - // TODO: fix underying signal value to int64 + // TODO: fix underlying signal value to int64 try await ctr.container.kill(Int32(try message.signal())) return message.reply() } diff --git a/Sources/TerminalProgress/ProgressBar.swift b/Sources/TerminalProgress/ProgressBar.swift index a49529655..5fc3b742b 100644 --- a/Sources/TerminalProgress/ProgressBar.swift +++ b/Sources/TerminalProgress/ProgressBar.swift @@ -190,7 +190,7 @@ extension ProgressBar { if config.showProgressBar, total > 0, allowProgress { let usedWidth = components.joined(separator: " ").count + 45 /* the maximum number of characters we may need */ - let remainingWidth = max(config.width - usedWidth, 1 /* the minumum width of a progress bar */) + let remainingWidth = max(config.width - usedWidth, 1 /* the minimum width of a progress bar */) let barLength = state.finished ? remainingWidth : Int(Int64(remainingWidth) * value / total) let barPaddingLength = remainingWidth - barLength let bar = "\(String(repeating: config.theme.bar, count: barLength))\(String(repeating: " ", count: barPaddingLength))" diff --git a/Sources/TerminalProgress/ProgressConfig.swift b/Sources/TerminalProgress/ProgressConfig.swift index 544604f11..88b4b7493 100644 --- a/Sources/TerminalProgress/ProgressConfig.swift +++ b/Sources/TerminalProgress/ProgressConfig.swift @@ -61,7 +61,7 @@ public struct ProgressConfig: Sendable { public let width: Int /// The theme of the progress bar. public let theme: ProgressTheme - /// The flag indicating whether to clear the progress bar before reseting the cursor. + /// The flag indicating whether to clear the progress bar before resetting the cursor. public let clearOnFinish: Bool /// The flag indicating whether to update the progress bar. public let disableProgressUpdates: Bool @@ -86,7 +86,7 @@ public struct ProgressConfig: Sendable { /// - totalSize: The initial total size of the progress bar. The default value is `nil`. /// - width: The width of the progress bar in characters. The default value is `120`. /// - theme: The theme of the progress bar. The default value is `nil`. - /// - clearOnFinish: The flag indicating whether to clear the progress bar before reseting the cursor. The default is `true`. + /// - clearOnFinish: The flag indicating whether to clear the progress bar before resetting the cursor. The default is `true`. /// - disableProgressUpdates: The flag indicating whether to update the progress bar. The default is `false`. public init( terminal: FileHandle = .standardError, diff --git a/Tests/ContainerBuildTests/GlobberTests.swift b/Tests/ContainerBuildTests/GlobberTests.swift index 24850382b..9ddcc1118 100644 --- a/Tests/ContainerBuildTests/GlobberTests.swift +++ b/Tests/ContainerBuildTests/GlobberTests.swift @@ -129,7 +129,7 @@ let testCases = [ func testGlobMatching(_ test: TestCase) throws { let globber = Globber(URL(fileURLWithPath: "/")) let found = try globber.glob(test.fileName, test.pattern) - #expect(found == test.expectSuccess, "expected found to be \(test.expectSuccess), instaed got \(found)") + #expect(found == test.expectSuccess, "expected found to be \(test.expectSuccess), instead got \(found)") } @Test("Invalid computed regex patterns throw error", arguments: errorGlobTestCases) From e6b7a29e9d4278a2ae8eddc7273a4770ee662e6b Mon Sep 17 00:00:00 2001 From: Danny Canter Date: Fri, 13 Jun 2025 12:03:59 -0700 Subject: [PATCH 19/45] README: Add project status (#192) Just as Containerization does, we should advertise the status of the project and source/tool stability guarantees for version numbers. --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 594a6cf59..dcfe847d8 100644 --- a/README.md +++ b/README.md @@ -55,3 +55,7 @@ uninstall-container.sh -k ## Contributing Contributions to `container` are welcomed and encouraged. Please see our [main contributing guide](https://github.com/apple/containerization/blob/main/CONTRIBUTING.md) for more information. + +## Project Status + +The container project is currently under active development. Its stability, both for consuming the project as a Swift package and the `container` tool, is only guaranteed within minor versions, such as between 0.1.1 and 0.1.2. Minor version number releases may include breaking changes until we achieve a 1.0.0 release. From c5392e838f4847c5c7e14b6144c6e55aadbc73f1 Mon Sep 17 00:00:00 2001 From: Aditya Ramani Date: Fri, 13 Jun 2025 13:45:25 -0700 Subject: [PATCH 20/45] Remove the system restart command (#196) Closes https://github.com/apple/container/issues/175 Also does some housekeeping - update availability checks and fix a typo Signed-off-by: Aditya Ramani --- Sources/CLI/System/SystemCommand.swift | 1 - Sources/CLI/System/SystemRestart.swift | 51 ------------------- .../NonisolatedInterfaceStrategy.swift | 2 +- .../RuntimeLinux/RuntimeLinuxHelper.swift | 2 +- .../SandboxService.swift | 4 +- 5 files changed, 4 insertions(+), 56 deletions(-) delete mode 100644 Sources/CLI/System/SystemRestart.swift diff --git a/Sources/CLI/System/SystemCommand.swift b/Sources/CLI/System/SystemCommand.swift index c884c8279..3a92bfb92 100644 --- a/Sources/CLI/System/SystemCommand.swift +++ b/Sources/CLI/System/SystemCommand.swift @@ -24,7 +24,6 @@ extension Application { subcommands: [ SystemDNS.self, SystemLogs.self, - SystemRestart.self, SystemStart.self, SystemStop.self, SystemStatus.self, diff --git a/Sources/CLI/System/SystemRestart.swift b/Sources/CLI/System/SystemRestart.swift deleted file mode 100644 index 91307be11..000000000 --- a/Sources/CLI/System/SystemRestart.swift +++ /dev/null @@ -1,51 +0,0 @@ -//===----------------------------------------------------------------------===// -// Copyright © 2025 Apple Inc. and the container project authors. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// https://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -//===----------------------------------------------------------------------===// - -import ArgumentParser -import ContainerClient -import ContainerPlugin -import ContainerizationError -import Foundation -import Logging - -extension Application { - struct SystemRestart: AsyncParsableCommand { - static let configuration = CommandConfiguration( - commandName: "restart", - abstract: "Restart API server for `container`" - ) - - @Option(name: .shortAndLong, help: "Launchd prefix for `container` services") - var prefix: String = "com.apple.container." - - func run() async throws { - let launchdDomainString = try ServiceManager.getDomainString() - let fullLabel = "\(launchdDomainString)/\(prefix)apiserver" - try ServiceManager.kickstart(fullServiceLabel: fullLabel) - // Now ping our friendly daemon. Fail after 10 seconds with no response. - do { - print("Verifying apiserver is running...") - try await ClientHealthCheck.ping(timeout: .seconds(10)) - print("Done") - } catch { - throw ContainerizationError( - .internalError, - message: "failed to get a response from apiserver after 10 seconds: \(error)" - ) - } - } - } -} diff --git a/Sources/Helpers/RuntimeLinux/NonisolatedInterfaceStrategy.swift b/Sources/Helpers/RuntimeLinux/NonisolatedInterfaceStrategy.swift index f6a00bdef..467f1c6d0 100644 --- a/Sources/Helpers/RuntimeLinux/NonisolatedInterfaceStrategy.swift +++ b/Sources/Helpers/RuntimeLinux/NonisolatedInterfaceStrategy.swift @@ -25,7 +25,7 @@ import vmnet #if !CURRENT_SDK /// Interface strategy for containers that use macOS's custom network feature. -@available(macOS 16, *) +@available(macOS 26, *) struct NonisolatedInterfaceStrategy: InterfaceStrategy { private let log: Logger diff --git a/Sources/Helpers/RuntimeLinux/RuntimeLinuxHelper.swift b/Sources/Helpers/RuntimeLinux/RuntimeLinuxHelper.swift index eeefde6be..6bd7a1602 100644 --- a/Sources/Helpers/RuntimeLinux/RuntimeLinuxHelper.swift +++ b/Sources/Helpers/RuntimeLinux/RuntimeLinuxHelper.swift @@ -64,7 +64,7 @@ struct RuntimeLinuxHelper: AsyncParsableCommand { log.info("configuring XPC server") let interfaceStrategy: any InterfaceStrategy #if !CURRENT_SDK - if #available(macOS 16, *) { + if #available(macOS 26, *) { interfaceStrategy = NonisolatedInterfaceStrategy(log: log) } else { interfaceStrategy = IsolatedInterfaceStrategy() diff --git a/Sources/Services/ContainerSandboxService/SandboxService.swift b/Sources/Services/ContainerSandboxService/SandboxService.swift index 6e901e715..8fceeb679 100644 --- a/Sources/Services/ContainerSandboxService/SandboxService.swift +++ b/Sources/Services/ContainerSandboxService/SandboxService.swift @@ -227,7 +227,7 @@ public actor SandboxService { stdout: stdio[1], stderr: stdio[2] ) - try await self.setUnderlingProcess(id, process) + try await self.setUnderlyingProcess(id, process) try await process.start() let waitFunc: ExitMonitor.WaitHandler = { try await process.wait() @@ -866,7 +866,7 @@ extension SandboxService { self.waiters[id] = [] } - private func setUnderlingProcess(_ id: String, _ process: LinuxProcess) throws { + private func setUnderlyingProcess(_ id: String, _ process: LinuxProcess) throws { guard var info = self.processes[id] else { throw ContainerizationError(.invalidState, message: "Process \(id) not found") } From 24730167da7aa638430a7e57e8d6b39d895d33f8 Mon Sep 17 00:00:00 2001 From: Satyam Singh <68936323+Thedarkmatter10@users.noreply.github.com> Date: Sat, 14 Jun 2025 02:17:24 +0530 Subject: [PATCH 21/45] =?UTF-8?q?fix(common.yml):=20globalize=20CURRENT=5F?= =?UTF-8?q?SDK,=20improve=20shell=20safety=20and=20=20imp=E2=80=A6=20(#178?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 🔧 Improvements Summary This PR introduces three improvements focused on safety, maintainability, and readability of the GitHub Actions workflow. --- ### 1. Define Global Environment Variable **Before:** Environment variable `CURRENT_SDK` was defined repeatedly in multiple steps. **After:** Declared in gloabally one time. #### Why This Matters: - Eliminates duplication across steps. - Makes it easier to update or remove the variable in the future. - Still allows per-step override when necessary. ### 2. Fix Unsafe Shell Conditional on inputs.release - Using **[[ ... ]]** **instea**d of **[ ... ]** for conditionals. - Adding **double quotes** around inputs and refs to **avoid** evaluation issues. **PREVIOUSLY FIXED** Containerization project. [https://github.com/apple/containerization/pull/68](url) ### 3. Removed EXCLUSION AND TODO comment. #### Affected Steps: `check Formatting` `make proto` ### Improvements: Now that the repositories are public, we no longer need to exclude files like Package.swift and Package.resolved from formatting and proto checks. - Removed EXCLUDES logic - Removed related TODO comments - Updated git diff checks to include all files @wlan0 @katiewasnothere --- .github/workflows/common.yml | 38 ++++++++++++++++++++++++++---------- 1 file changed, 28 insertions(+), 10 deletions(-) diff --git a/.github/workflows/common.yml b/.github/workflows/common.yml index 67dbf8342..7383f21ee 100644 --- a/.github/workflows/common.yml +++ b/.github/workflows/common.yml @@ -16,64 +16,79 @@ jobs: permissions: contents: read packages: read + env: + CURRENT_SDK: y steps: - name: Checkout repository uses: actions/checkout@v4 with: fetch-depth: 0 + - name: Update containerization run: | /usr/bin/swift package update containerization + - name: Check formatting run: | ./scripts/install-hawkeye.sh make fmt - if ! git diff --quiet -- . ':(exclude)Package.swift' ':(exclude)Package.resolved'; then echo "The following files require formatting or license header updates:\n$(git diff --name-only)" ; false ; fi + + if ! git diff --quiet -- . ; then + echo "❌ The following files require formatting or license header updates:" + git diff --name-only -- . + false + fi + - name: Check protobuf run: | make protos - - # TODO [launch]: TEMPORARILY we need to exclude these files since we had to modify them to add - # the github token for pulling the private repos. - if ! git diff --quiet -- . ':(exclude)Package.swift' ':(exclude)Package.resolved' ':(exclude)Protobuf.Makefile'; then echo "The following files require formatting or license header updates:\n$(git diff --name-only)" ; false ; fi - env: - CURRENT_SDK: y + if ! git diff --quiet -- . ; then + echo "❌ The following files require formatting or license header updates:" + git diff --name-only -- . + false + fi + - name: Set build configuration run: | echo "BUILD_CONFIGURATION=debug" >> $GITHUB_ENV - if [ ${{ inputs.release }} == true ]; then + if [[ "${{ inputs.release }}" == "true" ]]; then echo "BUILD_CONFIGURATION=release" >> $GITHUB_ENV fi + - name: Make the container project and docs run: | make container dsym docs tar cfz _site.tgz _site env: DEVELOPER_DIR: "/Applications/Xcode_16.3.app/Contents/Developer" - CURRENT_SDK: y + - name: Create package run: | mkdir -p outputs mv bin/${{ env.BUILD_CONFIGURATION }}/container-installer-unsigned.pkg outputs mv bin/${{ env.BUILD_CONFIGURATION }}/bundle/container-dSYM.zip outputs + - name: Test the container project run: | launchctl setenv HTTP_PROXY $HTTP_PROXY make test cleancontent install-kernel integration env: DEVELOPER_DIR: "/Applications/Xcode_16.3.app/Contents/Developer" - CURRENT_SDK: y + CURRENT_SDK: y # explicitly repeated due to local env block + - name: Save documentation artifact uses: actions/upload-artifact@v4 with: name: api-docs path: "./_site.tgz" retention-days: 14 + - name: Save package artifacts uses: actions/upload-artifact@v4 with: name: container-package path: ${{ github.workspace }}/outputs + uploadPages: # Separate upload step required because upload-pages-artifact needs # gtar which is not on the macOS runner. @@ -84,13 +99,16 @@ jobs: steps: - name: Setup Pages uses: actions/configure-pages@v5 + - name: Download a single artifact uses: actions/download-artifact@v4 with: name: api-docs + - name: Add API docs to documentation run: | tar xfz _site.tgz + - name: Upload Artifact uses: actions/upload-pages-artifact@v3 with: From a84389e51762be07c5e657b007336b382ee057a6 Mon Sep 17 00:00:00 2001 From: Kathryn Baldauf Date: Fri, 13 Jun 2025 14:47:16 -0700 Subject: [PATCH 22/45] Remove editor specific git ignore rules (#197) These should be up to users to maintain, as trying to add everyones editor of choice config directories here isn't ideal. Matches https://github.com/apple/containerization/pull/106 Signed-off-by: Kathryn Baldauf --- .gitignore | 3 --- 1 file changed, 3 deletions(-) diff --git a/.gitignore b/.gitignore index 964c7ce6f..43bf9f1e5 100644 --- a/.gitignore +++ b/.gitignore @@ -12,9 +12,6 @@ Packages/ api-docs/ workdir/ installer/ -.xcode/ -.vscode/ -.idea/ .venv/ .clitests/ test_results/ From 098021fc5b836552b910dc9bd06b9a82b5388c26 Mon Sep 17 00:00:00 2001 From: Noritaka Kobayashi Date: Sat, 14 Jun 2025 08:42:07 +0900 Subject: [PATCH 23/45] refactor: fix typos (#77) fix typos --- Sources/ContainerClient/Flags.swift | 2 +- .../CLITests/Subcommands/Build/CLIBuilderTest.swift | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Sources/ContainerClient/Flags.swift b/Sources/ContainerClient/Flags.swift index 0efcbd75b..5bbbfaabd 100644 --- a/Sources/ContainerClient/Flags.swift +++ b/Sources/ContainerClient/Flags.swift @@ -76,7 +76,7 @@ public struct Flags { self.scheme = scheme } - @Option(help: "Scheme to use when conntecting to the container registry. One of (http, https, auto)") + @Option(help: "Scheme to use when connecting to the container registry. One of (http, https, auto)") public var scheme: String = "auto" } diff --git a/Tests/CLITests/Subcommands/Build/CLIBuilderTest.swift b/Tests/CLITests/Subcommands/Build/CLIBuilderTest.swift index a37cdc4d4..26247192b 100644 --- a/Tests/CLITests/Subcommands/Build/CLIBuilderTest.swift +++ b/Tests/CLITests/Subcommands/Build/CLIBuilderTest.swift @@ -86,7 +86,7 @@ extension TestCLIBuildBase { """ let context: [FileSystemEntry] = [.file("emptyFile", content: .zeroFilled(size: 1))] try createContext(tempDir: tempDir, dockerfile: dockerfile, context: context) - let imageName = "regitry.local/scratch-add:\(UUID().uuidString)" + let imageName = "registry.local/scratch-add:\(UUID().uuidString)" try self.build(tag: imageName, tempDir: tempDir) #expect(try self.inspectImage(imageName) == imageName, "expected to have successfully built \(imageName)") } @@ -108,7 +108,7 @@ extension TestCLIBuildBase { .file("emptyFile", content: .zeroFilled(size: 1)), ] try createContext(tempDir: tempDir, dockerfile: dockerfile, context: context) - let imageName: String = "regitry.local/add-all:\(UUID().uuidString)" + let imageName: String = "registry.local/add-all:\(UUID().uuidString)" try self.build(tag: imageName, tempDir: tempDir) #expect(try self.inspectImage(imageName) == imageName, "expected to have successfully built \(imageName)") } @@ -122,7 +122,7 @@ extension TestCLIBuildBase { RUN nc -zv ${ADDRESS%:*} ${ADDRESS##*:} || exit 1 """ try createContext(tempDir: tempDir, dockerfile: dockerfile) - let imageName = "regitry.local/build-network-access:\(UUID().uuidString)" + let imageName = "registry.local/build-network-access:\(UUID().uuidString)" let proxyEnv = ProcessInfo.processInfo.environment["HTTP_PROXY"] var address = "8.8.8.8:53" @@ -226,7 +226,7 @@ extension TestCLIBuildBase { ] try createContext(tempDir: tempDir, dockerfile: dockerfile, context: context) - let imageName = "regitry.local/dockerfile-keywords:\(UUID().uuidString)" + let imageName = "registry.local/dockerfile-keywords:\(UUID().uuidString)" try self.build(tag: imageName, tempDir: tempDir) #expect(try self.inspectImage(imageName) == imageName, "expected to have successfully built \(imageName)") } @@ -279,7 +279,7 @@ extension TestCLIBuildBase { .symbolicLink("Test3Source2/Dest", target: "Test3Source/Source"), ] try createContext(tempDir: tempDir, dockerfile: dockerfile, context: context) - let imageName = "regitry.local/build-symlinks:\(UUID().uuidString)" + let imageName = "registry.local/build-symlinks:\(UUID().uuidString)" #expect(throws: Never.self) { try self.build(tag: imageName, tempDir: tempDir) @@ -338,7 +338,7 @@ extension TestCLIBuildBase { ] try createContext(tempDir: buildContextDir, dockerfile: "", context: buildContext) - let imageName = "regitry.local/build-diff-context:\(UUID().uuidString)" + let imageName = "registry.local/build-diff-context:\(UUID().uuidString)" #expect(throws: Never.self) { try self.buildWithPaths(tag: imageName, tempContext: buildContextDir, tempDockerfileContext: dockerfileCtxDir) } From 05798f38a18e4d01d6f1b8b0a32f3db592b26073 Mon Sep 17 00:00:00 2001 From: Dmitry Kovba Date: Fri, 13 Jun 2025 16:58:28 -0700 Subject: [PATCH 24/45] Update the names of Xcode and macOS (#123) This PR updates the names of Xcode and macOS to the official names used in https://developer.apple.com/download/applications/. --- BUILDING.md | 4 ++-- README.md | 2 +- docs/technical-overview.md | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/BUILDING.md b/BUILDING.md index 1d15e1298..9d05315ae 100644 --- a/BUILDING.md +++ b/BUILDING.md @@ -2,8 +2,8 @@ To build the `container` project, your system needs either: -- macOS 15 or newer and Xcode 26 Beta -- macOS 26 Beta 1 or newer +- macOS 15 or newer and Xcode 26 beta +- macOS 26 beta or newer ## Compile and test diff --git a/README.md b/README.md index dcfe847d8..56c3da31b 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ The tool consumes and produces OCI-compliant container images, so you can pull a You need an Apple silicon Mac to run `container`. To build it, see the [BUILDING](./BUILDING.md) document. -`container` relies on the new features and enhancements present in the macOS 26 Beta 1. You can run the tool on macOS 15, but the `container` maintainers typically will not address issues discovered on macOS 15 that cannot be reproduced on the macOS 26 Beta 1. +`container` relies on the new features and enhancements present in the macOS 26 beta. You can run the tool on macOS 15, but the `container` maintainers typically will not address issues discovered on macOS 15 that cannot be reproduced on the macOS 26 beta. There are [significant networking limitations](/docs/technical-overview.md#macos-15-limitations) that impact the usability of `container` on macOS 15. diff --git a/docs/technical-overview.md b/docs/technical-overview.md index 55e2eda66..897780e61 100644 --- a/docs/technical-overview.md +++ b/docs/technical-overview.md @@ -67,7 +67,7 @@ Currently, memory pages freed to the Linux operating system by processes running ### macOS 15 limitations -`container` relies on the new features and enhancements present in the macOS 26 Beta 1. You can run `container` on macOS 15, but you will need to be aware of some user experience and functional limitations. There is no plan to address issues found with macOS 15 that cannot be reproduced in the macOS 26 Beta 1. +`container` relies on the new features and enhancements present in the macOS 26 beta. You can run `container` on macOS 15, but you will need to be aware of some user experience and functional limitations. There is no plan to address issues found with macOS 15 that cannot be reproduced in the macOS 26 beta. #### Network isolation From 84798601cd563f5314dd46375c73613a3b4e9c0a Mon Sep 17 00:00:00 2001 From: Eliseo Martelli Date: Sat, 14 Jun 2025 02:07:55 +0200 Subject: [PATCH 25/45] Throw errors in ServiceManager (#188) --- Sources/ContainerPlugin/ServiceManager.swift | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/Sources/ContainerPlugin/ServiceManager.swift b/Sources/ContainerPlugin/ServiceManager.swift index d7a936f3c..8c5710d11 100644 --- a/Sources/ContainerPlugin/ServiceManager.swift +++ b/Sources/ContainerPlugin/ServiceManager.swift @@ -62,21 +62,23 @@ public struct ServiceManager { let null = FileHandle.nullDevice let stdoutPipe = Pipe() + let stderrPipe = Pipe() launchctl.standardOutput = stdoutPipe - launchctl.standardError = null + launchctl.standardError = stderrPipe try launchctl.run() let outputData = stdoutPipe.fileHandleForReading.readDataToEndOfFile() + let stderrData = stderrPipe.fileHandleForReading.readDataToEndOfFile() launchctl.waitUntilExit() let status = launchctl.terminationStatus guard status == 0 else { - // TODO: review error handling - return [] + throw ContainerizationError( + .internalError, message: "Command `launchctl list` failed with status \(status). Message: \(String(data: stderrData, encoding: .utf8) ?? "No error message")") } guard let outputText = String(data: outputData, encoding: .utf8) else { - // TODO: review error handling - return [] + throw ContainerizationError( + .internalError, message: "Could not decode output of command `launchctl list`. Message: \(String(data: stderrData, encoding: .utf8) ?? "No error message")") } // The third field of each line of launchctl list output is the label From 51c1b879d2d0b2c14d8bf529cc134c0eca7a7284 Mon Sep 17 00:00:00 2001 From: Dmitry Kovba Date: Fri, 13 Jun 2025 17:27:50 -0700 Subject: [PATCH 26/45] Update to Swift 6.2 (#195) This PR updates to Swift 6.2 and resolves a build error after updating to Swift 6.2-snapshot in https://github.com/apple/containerization/pull/94. --- .github/workflows/common.yml | 14 ++++++-------- Package.resolved | 6 +++--- Package.swift | 2 +- scripts/install-init.sh | 10 +++++----- 4 files changed, 15 insertions(+), 17 deletions(-) diff --git a/.github/workflows/common.yml b/.github/workflows/common.yml index 7383f21ee..cc00da1fd 100644 --- a/.github/workflows/common.yml +++ b/.github/workflows/common.yml @@ -11,7 +11,7 @@ on: jobs: buildAndTest: name: Build and test the project - timeout-minutes: 30 + timeout-minutes: 60 runs-on: [self-hosted, macos, sequoia, ARM64] permissions: contents: read @@ -24,10 +24,6 @@ jobs: with: fetch-depth: 0 - - name: Update containerization - run: | - /usr/bin/swift package update containerization - - name: Check formatting run: | ./scripts/install-hawkeye.sh @@ -47,7 +43,9 @@ jobs: git diff --name-only -- . false fi - + env: + DEVELOPER_DIR: "/Applications/Xcode_26.b1.app/Contents/Developer" + - name: Set build configuration run: | echo "BUILD_CONFIGURATION=debug" >> $GITHUB_ENV @@ -60,7 +58,7 @@ jobs: make container dsym docs tar cfz _site.tgz _site env: - DEVELOPER_DIR: "/Applications/Xcode_16.3.app/Contents/Developer" + DEVELOPER_DIR: "/Applications/Xcode_26.b1.app/Contents/Developer" - name: Create package run: | @@ -73,7 +71,7 @@ jobs: launchctl setenv HTTP_PROXY $HTTP_PROXY make test cleancontent install-kernel integration env: - DEVELOPER_DIR: "/Applications/Xcode_16.3.app/Contents/Developer" + DEVELOPER_DIR: "/Applications/Xcode_26.b1.app/Contents/Developer" CURRENT_SDK: y # explicitly repeated due to local env block - name: Save documentation artifact diff --git a/Package.resolved b/Package.resolved index c6259c6cf..289c7fd2b 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "21dad499f34492edb861e54fe1e03ee3b00aa3d7371af6b09253bf04495427b9", + "originHash" : "4ac93777a9a369fb7c46f1af4cd15c516926a8f4b23679f1ee2bc40b9a422313", "pins" : [ { "identity" : "async-http-client", @@ -87,8 +87,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-atomics.git", "state" : { - "revision" : "cd142fd2f64be2100422d658e7411e39489da985", - "version" : "1.2.0" + "revision" : "b601256eab081c0f92f059e12818ac1d4f178ff7", + "version" : "1.3.0" } }, { diff --git a/Package.swift b/Package.swift index 288ed99ba..8fa4ba087 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version: 6.0 +// swift-tools-version: 6.2 //===----------------------------------------------------------------------===// // Copyright © 2025 Apple Inc. and the container project authors. All rights reserved. // diff --git a/scripts/install-init.sh b/scripts/install-init.sh index 6cdd3f56e..3f8e2265c 100755 --- a/scripts/install-init.sh +++ b/scripts/install-init.sh @@ -13,18 +13,18 @@ # See the License for the specific language governing permissions and # limitations under the License. +SWIFT="/usr/bin/swift" +IMAGE_NAME="vminit:latest" DESTDIR="${1:-$(git rev-parse --show-toplevel)/bin}" mkdir -p "${DESTDIR}" -IMAGE_NAME="vminit:latest" - -CONTAINERIZATION_VERSION="${CONTAINERIZATION_VERSION:-$(swift package show-dependencies --format json | jq -r '.dependencies[] | select(.identity == "containerization") | .version')}" +CONTAINERIZATION_VERSION="${CONTAINERIZATION_VERSION:-$(${SWIFT} package show-dependencies --format json | jq -r '.dependencies[] | select(.identity == "containerization") | .version')}" if [ ! -z "${CONTAINERIZATION_PATH}" -o "${CONTAINERIZATION_VERSION}" == "unspecified" ] ; then - CONTAINERIZATION_PATH="${CONTAINERIZATION_PATH:-$(swift package show-dependencies --format json | jq -r '.dependencies[] | select(.identity == "containerization") | .path')}" + CONTAINERIZATION_PATH="${CONTAINERIZATION_PATH:-$(${SWIFT} package show-dependencies --format json | jq -r '.dependencies[] | select(.identity == "containerization") | .path')}" echo "Creating InitImage" make -C ${CONTAINERIZATION_PATH} init ${CONTAINERIZATION_PATH}/bin/cctl images save -o /tmp/init.tar ${IMAGE_NAME} - # sleep because commands after stop and start are racy + # Sleep because commands after stop and start are racy. bin/container system stop && sleep 3 && bin/container system start && sleep 3 bin/container i load -i /tmp/init.tar rm /tmp/init.tar From 555dbfc9fa1ec9f1050086b892fe9d5da049f7a0 Mon Sep 17 00:00:00 2001 From: Dmitry Kovba Date: Fri, 13 Jun 2025 19:48:59 -0700 Subject: [PATCH 27/45] Adds a warning when running a debug build (#201) This PR adds a warning that the performance may be degraded when running a debug build. Debug build: ``` % bin/container run -it --rm alpine:latest date Warning! Running debug build. Performance may be degraded. Fri Jun 13 18:10:35 PDT 2025 ``` Release build: ``` % bin/container run -it --rm alpine:latest date Fri Jun 13 18:10:35 PDT 2025 ``` --- Sources/CLI/Application.swift | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Sources/CLI/Application.swift b/Sources/CLI/Application.swift index 3d029bcff..3631de5a1 100644 --- a/Sources/CLI/Application.swift +++ b/Sources/CLI/Application.swift @@ -150,6 +150,13 @@ struct Application: AsyncParsableCommand { public static func main() async throws { restoreCursorAtExit() + #if DEBUG + let warning = "Running debug build. Performance may be degraded." + let formattedWarning = "\u{001B}[33mWarning!\u{001B}[0m \(warning)\n" + let warningData = Data(formattedWarning.utf8) + FileHandle.standardError.write(warningData) + #endif + let fullArgs = CommandLine.arguments let args = Array(fullArgs.dropFirst()) From 4d6f7037f3cf4d7c308e8682da547fd37c90167d Mon Sep 17 00:00:00 2001 From: Danny Canter Date: Mon, 16 Jun 2025 08:11:25 -0700 Subject: [PATCH 28/45] Plugins: Remove unused devnull var (#214) We actually use the output (both stderr and stdout) of the command. --- Sources/ContainerPlugin/ServiceManager.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/Sources/ContainerPlugin/ServiceManager.swift b/Sources/ContainerPlugin/ServiceManager.swift index 8c5710d11..73071fa72 100644 --- a/Sources/ContainerPlugin/ServiceManager.swift +++ b/Sources/ContainerPlugin/ServiceManager.swift @@ -60,7 +60,6 @@ public struct ServiceManager { launchctl.executableURL = URL(fileURLWithPath: "/bin/launchctl") launchctl.arguments = ["list"] - let null = FileHandle.nullDevice let stdoutPipe = Pipe() let stderrPipe = Pipe() launchctl.standardOutput = stdoutPipe From 4a4ad40d749ffe643fdfd3edbf80e878572383d9 Mon Sep 17 00:00:00 2001 From: Josh Soref <2119212+jsoref@users.noreply.github.com> Date: Mon, 16 Jun 2025 13:49:03 -0400 Subject: [PATCH 29/45] Spelling (#207) This PR corrects misspellings identified by the [check-spelling action](https://github.com/marketplace/actions/check-spelling) The misspellings have been reported at https://github.com/jsoref/container/actions/runs/15662939575/attempts/1 The action reports that the changes in this PR would make it happy: https://github.com/jsoref/container/actions/runs/15662939742/attempts/1#summary-44123289718 --------- Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com> --- Makefile | 2 +- Sources/APIServer/APIServer.swift | 6 +++--- Sources/CLI/Application.swift | 2 +- Sources/CLI/Container/ContainerDelete.swift | 4 ++-- Sources/ContainerClient/Core/ClientImage.swift | 2 +- Sources/ContainerPlugin/PluginLoader.swift | 4 ++-- Sources/TerminalProgress/ProgressBar+Add.swift | 2 +- Sources/TerminalProgress/ProgressConfig.swift | 2 +- 8 files changed, 12 insertions(+), 12 deletions(-) diff --git a/Makefile b/Makefile index 500c1f8c0..2a4214572 100644 --- a/Makefile +++ b/Makefile @@ -49,7 +49,7 @@ all: init-block .PHONY: build build: @echo Building container binaries... - @#Remove this when the updated MacOS SDK is available publicly + @#Remove this when the updated macOS SDK is available publicly $(SWIFT) build -c $(BUILD_CONFIGURATION) $(CURRENT_SDK_ARGS) ; \ .PHONY: container diff --git a/Sources/APIServer/APIServer.swift b/Sources/APIServer/APIServer.swift index c771c9462..3edb85a91 100644 --- a/Sources/APIServer/APIServer.swift +++ b/Sources/APIServer/APIServer.swift @@ -182,9 +182,9 @@ struct APIServer: AsyncParsableCommand { private func initializeKernelService(log: Logger, routes: inout [XPCRoute: XPCServer.RouteHandler]) throws { let svc = try KernelService(log: log, appRoot: Self.appRoot) - let harnsess = KernelHarness(service: svc, log: log) - routes[XPCRoute.installKernel] = harnsess.install - routes[XPCRoute.getDefaultKernel] = harnsess.getDefaultKernel + let harness = KernelHarness(service: svc, log: log) + routes[XPCRoute.installKernel] = harness.install + routes[XPCRoute.getDefaultKernel] = harness.getDefaultKernel } private func initializeContainerService(root: URL, pluginLoader: PluginLoader, log: Logger, routes: inout [XPCRoute: XPCServer.RouteHandler]) throws { diff --git a/Sources/CLI/Application.swift b/Sources/CLI/Application.swift index 3631de5a1..a153acd29 100644 --- a/Sources/CLI/Application.swift +++ b/Sources/CLI/Application.swift @@ -294,7 +294,7 @@ extension Application { versionDetails["build"] = "debug" #endif #if CURRENT_SDK - versionDetails["sdk"] = "MacOS 15" + versionDetails["sdk"] = "macOS 15" #endif let gitCommit = { let sha = get_git_commit().map { String(cString: $0) } diff --git a/Sources/CLI/Container/ContainerDelete.swift b/Sources/CLI/Container/ContainerDelete.swift index 0da563770..eb7baf280 100644 --- a/Sources/CLI/Container/ContainerDelete.swift +++ b/Sources/CLI/Container/ContainerDelete.swift @@ -61,8 +61,8 @@ extension Application { containers = ctrs.filter { c in set.contains(c.id) } - // If one of the containers requested isn't present lets throw. We don't need to do - // this for --all as --all should be perfectly usable with no containers to remove, otherwise + // If one of the containers requested isn't present, let's throw. We don't need to do + // this for --all as --all should be perfectly usable with no containers to remove; otherwise, // it'd be quite clunky. if containers.count != set.count { let missing = set.filter { id in diff --git a/Sources/ContainerClient/Core/ClientImage.swift b/Sources/ContainerClient/Core/ClientImage.swift index 684f302c3..386bc118a 100644 --- a/Sources/ContainerClient/Core/ClientImage.swift +++ b/Sources/ContainerClient/Core/ClientImage.swift @@ -99,7 +99,7 @@ extension ClientImage { public static func normalizeReference(_ ref: String) throws -> String { guard ref != Self.initImageRef else { - // Don't modify the the default init image reference. + // Don't modify the default init image reference. // This is to allow for easier local development against // an updated containerization. return ref diff --git a/Sources/ContainerPlugin/PluginLoader.swift b/Sources/ContainerPlugin/PluginLoader.swift index 6a8b97b0d..db86036a8 100644 --- a/Sources/ContainerPlugin/PluginLoader.swift +++ b/Sources/ContainerPlugin/PluginLoader.swift @@ -169,7 +169,7 @@ extension PluginLoader { instanceId: String? = nil ) throws { // We only care about loading plugins that have a service - // to expose, otherwise they may just be CLI commands. + // to expose; otherwise, they may just be CLI commands. guard let serviceConfig = plugin.config.servicesConfig else { return } @@ -201,7 +201,7 @@ extension PluginLoader { public func deregisterWithLaunchd(plugin: Plugin, instanceId: String? = nil) throws { // We only care about loading plugins that have a service - // to expose, otherwise they may just be CLI commands. + // to expose; otherwise, they may just be CLI commands. guard plugin.config.servicesConfig != nil else { return } diff --git a/Sources/TerminalProgress/ProgressBar+Add.swift b/Sources/TerminalProgress/ProgressBar+Add.swift index 7f5a68f94..8fef8baa4 100644 --- a/Sources/TerminalProgress/ProgressBar+Add.swift +++ b/Sources/TerminalProgress/ProgressBar+Add.swift @@ -64,7 +64,7 @@ extension ProgressBar { var finished = true var defined = false if let totalTasks = state.totalTasks, totalTasks > 0 { - // For tasks, we're showing the current task rather then the number of completed tasks. + // For tasks, we're showing the current task rather than the number of completed tasks. finished = finished && state.tasks == totalTasks defined = true } diff --git a/Sources/TerminalProgress/ProgressConfig.swift b/Sources/TerminalProgress/ProgressConfig.swift index 88b4b7493..353cc339a 100644 --- a/Sources/TerminalProgress/ProgressConfig.swift +++ b/Sources/TerminalProgress/ProgressConfig.swift @@ -27,7 +27,7 @@ public struct ProgressConfig: Sendable { /// The initial items name (e.g., "files"). let initialItemsName: String /// A flag indicating whether to show a spinner (e.g., "⠋"). - /// The spinner is hidden when when a progress bar is shown. + /// The spinner is hidden when a progress bar is shown. public let showSpinner: Bool /// A flag indicating whether to show tasks and total tasks (e.g., "[1]" or "[1/3]"). public let showTasks: Bool From 011e6764f7766808a3e74c73f2e61062756076d9 Mon Sep 17 00:00:00 2001 From: Michael Crosby Date: Mon, 16 Jun 2025 14:03:57 -0400 Subject: [PATCH 30/45] use rotatingAllocator for ipam (#217) Signed-off-by: crosbymichael --- .../ContainerNetworkService/AttachmentAllocator.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/Services/ContainerNetworkService/AttachmentAllocator.swift b/Sources/Services/ContainerNetworkService/AttachmentAllocator.swift index 45e0afe1c..4d2bee886 100644 --- a/Sources/Services/ContainerNetworkService/AttachmentAllocator.swift +++ b/Sources/Services/ContainerNetworkService/AttachmentAllocator.swift @@ -22,9 +22,9 @@ actor AttachmentAllocator { private var hostnames: [String: UInt32] = [:] init(lower: UInt32, size: Int) throws { - allocator = try UInt32.allocator( + allocator = try UInt32.rotatingAllocator( lower: lower, - size: size + size: UInt32(size) ) } From 009ce93b4a7ed935ad276b829b9cc59fbbbbaa87 Mon Sep 17 00:00:00 2001 From: Dmitry Kovba Date: Mon, 16 Jun 2025 12:35:14 -0700 Subject: [PATCH 31/45] Require having a Mac with Apple silicon and Xcode 26 beta (#125) This PR requires installing Xcode 26 beta until we resolve https://github.com/apple/containerization/issues/66. --- BUILDING.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/BUILDING.md b/BUILDING.md index 9d05315ae..3b439670e 100644 --- a/BUILDING.md +++ b/BUILDING.md @@ -1,9 +1,10 @@ # Building the project -To build the `container` project, your system needs either: +To build the `container` project, you need: -- macOS 15 or newer and Xcode 26 beta -- macOS 26 beta or newer +- Mac with Apple silicon +- macOS 15 minimum, macOS 26 beta recommended +- Xcode 26 beta ## Compile and test From 157bacf6a4fc1f88069ad7ec546453bf5cda6021 Mon Sep 17 00:00:00 2001 From: Kathryn Baldauf Date: Mon, 16 Jun 2025 12:38:55 -0700 Subject: [PATCH 32/45] Make test suites run sequentially with respect to other test suites (#200) Right now swift testing does not have finer grain test parallelization controls. As a result our easy options are either to have ALL tests in ALL test suites run sequentially or have tests within a given test suite run sequentially while other test suites are run at the same time. This has been problematic for our CI since we opted for the second option above, where, for examples, the tests in the builder test suite run sequentially, but the builder test suite runs at the same time as the container run test suite. When this happens, a lot of different tests try to pull the necessary images for testing at the same time, causing some tests to timeout. Signed-off-by: Kathryn Baldauf --- Makefile | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 2a4214572..2eba0398c 100644 --- a/Makefile +++ b/Makefile @@ -143,7 +143,12 @@ integration: init-block @echo "Removing any existing containers" @bin/container rm --all @echo "Starting CLI integration tests" - @RUN_CLI_INTEGRATION_TESTS=1 $(SWIFT) test -c $(BUILD_CONFIGURATION) $(CURRENT_SDK_ARGS) --filter TestCLI + @$(SWIFT) test -c $(BUILD_CONFIGURATION) $(CURRENT_SDK_ARGS) --filter TestCLIRunLifecycle + @$(SWIFT) test -c $(BUILD_CONFIGURATION) $(CURRENT_SDK_ARGS) --filter TestCLIExecCommand + @$(SWIFT) test -c $(BUILD_CONFIGURATION) $(CURRENT_SDK_ARGS) --filter TestCLIRunCommand + @$(SWIFT) test -c $(BUILD_CONFIGURATION) $(CURRENT_SDK_ARGS) --filter TestCLIImagesCommand + @$(SWIFT) test -c $(BUILD_CONFIGURATION) $(CURRENT_SDK_ARGS) --filter TestCLIRunBase + @$(SWIFT) test -c $(BUILD_CONFIGURATION) $(CURRENT_SDK_ARGS) --filter TestCLIBuildBase @echo Ensuring apiserver stopped after the CLI integration tests... @scripts/ensure-container-stopped.sh From 4c171d464157e2d9c83c27859e971c2796616b5f Mon Sep 17 00:00:00 2001 From: Aditya Ramani Date: Mon, 16 Jun 2025 13:41:56 -0700 Subject: [PATCH 33/45] Wait for IO streams to complete before a process exits (#198) Implements a way for the CLI to wait until the IO streams from the SandboxService have been drained before closing them. Follows the same pattern as https://github.com/apple/containerization/pull/110 This change also performs some cleanup in the `SandboxService.startProcess` method - splitting the code paths to handle the init process and an exec'd process into two different private methods to make easier reading --------- Signed-off-by: Aditya Ramani --- Sources/CLI/Application.swift | 4 +- Sources/CLI/RunCommand.swift | 42 +++++ .../SandboxService.swift | 175 +++++++++++------- 3 files changed, 149 insertions(+), 72 deletions(-) diff --git a/Sources/CLI/Application.swift b/Sources/CLI/Application.swift index a153acd29..ea3e7a94e 100644 --- a/Sources/CLI/Application.swift +++ b/Sources/CLI/Application.swift @@ -184,7 +184,9 @@ struct Application: AsyncParsableCommand { let signals = AsyncSignalHandler.create(notify: Application.signalSet) return try await withThrowingTaskGroup(of: Int32?.self, returning: Int32.self) { group in let waitAdded = group.addTaskUnlessCancelled { - try await process.wait() + let code = try await process.wait() + try await io.wait() + return code } guard waitAdded else { diff --git a/Sources/CLI/RunCommand.swift b/Sources/CLI/RunCommand.swift index 822f72058..3a818e939 100644 --- a/Sources/CLI/RunCommand.swift +++ b/Sources/CLI/RunCommand.swift @@ -18,6 +18,7 @@ import ArgumentParser import ContainerClient import Containerization import ContainerizationError +import ContainerizationExtras import ContainerizationOS import Foundation import NIOCore @@ -169,6 +170,13 @@ struct ProcessIO { let stdin: Pipe? let stdout: Pipe? let stderr: Pipe? + var ioTracker: IoTracker? + + struct IoTracker { + let stream: AsyncStream + let cont: AsyncStream.Continuation + let configuredStreams: Int + } let stdio: [FileHandle?] @@ -224,7 +232,11 @@ struct ProcessIO { } return Pipe() }() + + var configuredStreams = 0 + let (stream, cc) = AsyncStream.makeStream() if let stdout { + configuredStreams += 1 let pout: FileHandle = { if let current { return current.handle @@ -237,6 +249,7 @@ struct ProcessIO { let data = handle.availableData if data.isEmpty { rout.readabilityHandler = nil + cc.yield() return } try! pout.write(contentsOf: data) @@ -251,12 +264,14 @@ struct ProcessIO { return Pipe() }() if let stderr { + configuredStreams += 1 let perr: FileHandle = .standardError let rerr = stderr.fileHandleForReading rerr.readabilityHandler = { handle in let data = handle.availableData if data.isEmpty { rerr.readabilityHandler = nil + cc.yield() return } try! perr.write(contentsOf: data) @@ -264,12 +279,39 @@ struct ProcessIO { stdio[2] = stderr.fileHandleForWriting } + var ioTracker: IoTracker? = nil + if configuredStreams > 0 { + ioTracker = .init(stream: stream, cont: cc, configuredStreams: configuredStreams) + } + return .init( stdin: stdin, stdout: stdout, stderr: stderr, + ioTracker: ioTracker, stdio: stdio, console: current ) } + + public func wait() async throws { + guard let ioTracker = self.ioTracker else { + return + } + do { + try await Timeout.run(seconds: 3) { + var counter = ioTracker.configuredStreams + for await _ in ioTracker.stream { + counter -= 1 + if counter == 0 { + ioTracker.cont.finish() + break + } + } + } + } catch { + log.error("Timeout waiting for IO to complete : \(error)") + throw error + } + } } diff --git a/Sources/Services/ContainerSandboxService/SandboxService.swift b/Sources/Services/ContainerSandboxService/SandboxService.swift index 8fceeb679..5a801c5fc 100644 --- a/Sources/Services/ContainerSandboxService/SandboxService.swift +++ b/Sources/Services/ContainerSandboxService/SandboxService.swift @@ -154,88 +154,111 @@ public actor SandboxService { @Sendable public func startProcess(_ message: XPCMessage) async throws -> XPCMessage { self.log.info("`start` xpc handler") - return try await self.lock.withLock { _ in + return try await self.lock.withLock { lock in let id = try message.id() let stdio = message.stdio() let containerInfo = try await self.getContainer() let containerId = containerInfo.container.id - let container = containerInfo.container - let bundle = containerInfo.bundle if id == containerId { - guard await self.state == .booted else { - throw ContainerizationError( - .invalidState, - message: "container expected to be in booted state, got: \(await self.state)" - ) - } - let containerLog = try FileHandle(forWritingTo: bundle.containerLog) - let config = containerInfo.config - let stdout = { - if let h = stdio[1] { - return MultiWriter(handles: [h, containerLog]) - } - return MultiWriter(handles: [containerLog]) - }() - let stderr: MultiWriter? = { - if !config.initProcess.terminal { - if let h = stdio[2] { - return MultiWriter(handles: [h, containerLog]) - } - return MultiWriter(handles: [containerLog]) - } - return nil - }() - if let h = stdio[0] { - container.stdin = h - } - container.stdout = stdout - if let stderr { - container.stderr = stderr - } - await self.setState(.starting) - do { - try await container.start() - let waitFunc: ExitMonitor.WaitHandler = { - let code = try await container.wait() - return code - } - try await self.monitor.track(id: id, waitingOn: waitFunc) - } catch { - try? await self.cleanupContainer() - await self.setState(.created) - try await self.sendContainerEvent(.containerExit(id: id, exitCode: -1)) - throw error - } + try await self.startInitProcess(stdio: stdio, lock: lock) await self.setState(.running) try await self.sendContainerEvent(.containerStart(id: id)) } else { - // we are starting a process other than the init process. Check if it exists - guard let processInfo = await self.processes[id] else { - throw ContainerizationError(.notFound, message: "Process with id \(id)") + try await self.startExecProcess(processId: id, stdio: stdio, lock: lock) + } + return message.reply() + } + } + + private func startInitProcess(stdio: [FileHandle?], lock: AsyncLock.Context) async throws { + let info = try self.getContainer() + let container = info.container + let bundle = info.bundle + let id = container.id + guard self.state == .booted else { + throw ContainerizationError( + .invalidState, + message: "container expected to be in booted state, got: \(self.state)" + ) + } + let containerLog = try FileHandle(forWritingTo: bundle.containerLog) + let config = info.config + let stdout = { + if let h = stdio[1] { + return MultiWriter(handles: [h, containerLog]) + } + return MultiWriter(handles: [containerLog]) + }() + let stderr: MultiWriter? = { + if !config.initProcess.terminal { + if let h = stdio[2] { + return MultiWriter(handles: [h, containerLog]) } - let ociConfig = self.configureProcessConfig(config: processInfo.config) - let stdin: ReaderStream? = { - if let h = stdio[0] { - return h - } - return nil - }() - let process = try await container.exec( - id, - configuration: ociConfig, - stdin: stdin, - stdout: stdio[1], - stderr: stdio[2] - ) - try await self.setUnderlyingProcess(id, process) - try await process.start() - let waitFunc: ExitMonitor.WaitHandler = { - try await process.wait() + return MultiWriter(handles: [containerLog]) + } + return nil + }() + if let h = stdio[0] { + container.stdin = h + } + container.stdout = stdout + if let stderr { + container.stderr = stderr + } + self.setState(.starting) + do { + try await container.start() + let waitFunc: ExitMonitor.WaitHandler = { + let code = try await container.wait() + if let out = stdio[1] { + try self.closeHandle(out.fileDescriptor) + } + if let err = stdio[2] { + try self.closeHandle(err.fileDescriptor) } - try await self.monitor.track(id: id, waitingOn: waitFunc) + return code } - return message.reply() + try await self.monitor.track(id: id, waitingOn: waitFunc) + } catch { + try? await self.cleanupContainer() + self.setState(.created) + try await self.sendContainerEvent(.containerExit(id: id, exitCode: -1)) + throw error + } + } + + private func startExecProcess(processId id: String, stdio: [FileHandle?], lock: AsyncLock.Context) async throws { + let container = try self.getContainer().container + guard let processInfo = self.processes[id] else { + throw ContainerizationError(.notFound, message: "Process with id \(id)") + } + let ociConfig = self.configureProcessConfig(config: processInfo.config) + let stdin: ReaderStream? = { + if let h = stdio[0] { + return h + } + return nil + }() + let process = try await container.exec( + id, + configuration: ociConfig, + stdin: stdin, + stdout: stdio[1], + stderr: stdio[2] + ) + try self.setUnderlyingProcess(id, process) + try await process.start() + let waitFunc: ExitMonitor.WaitHandler = { + let code = try await process.wait() + if let out = stdio[1] { + try self.closeHandle(out.fileDescriptor) + } + if let err = stdio[2] { + try self.closeHandle(err.fileDescriptor) + } + return code } + try await self.monitor.track(id: id, waitingOn: waitFunc) } /// Create a process inside the virtual machine for the container. @@ -267,13 +290,14 @@ public actor SandboxService { try await self.monitor.registerProcess( id: id, onExit: { id, code in - guard await self.processes[id] != nil else { + guard let process = await self.processes[id]?.process else { throw ContainerizationError(.invalidState, message: "ProcessInfo missing for process \(id)") } for cc in await self.waiters[id] ?? [] { cc.resume(returning: code) } await self.removeWaiters(for: id) + try await process.delete() try await self.setProcessState(id: id, state: .stopped(code)) }) return message.reply() @@ -664,6 +688,15 @@ public actor SandboxService { return proc } + private nonisolated func closeHandle(_ handle: Int32) throws { + guard close(handle) == 0 else { + guard let errCode = POSIXErrorCode(rawValue: errno) else { + fatalError("failed to convert errno to POSIXErrorCode") + } + throw POSIXError(errCode) + } + } + private nonisolated func modifyingEnvironment(_ config: ProcessConfiguration) -> [String] { guard config.terminal else { return config.environment From 0535d36929695628b87fea4c61c027077460a7a6 Mon Sep 17 00:00:00 2001 From: Dmitry Kovba Date: Mon, 16 Jun 2025 17:55:48 -0700 Subject: [PATCH 34/45] Fix warnings in `make docs` (#220) --- .../TerminalProgress/ProgressBar+Add.swift | 38 ++++++++++++------- 1 file changed, 25 insertions(+), 13 deletions(-) diff --git a/Sources/TerminalProgress/ProgressBar+Add.swift b/Sources/TerminalProgress/ProgressBar+Add.swift index 8fef8baa4..42d2e2d8a 100644 --- a/Sources/TerminalProgress/ProgressBar+Add.swift +++ b/Sources/TerminalProgress/ProgressBar+Add.swift @@ -82,7 +82,8 @@ extension ProgressBar { } /// Sets the current tasks. - /// - Parameter tasks: The current tasks to set. + /// - Parameter newTasks: The current tasks to set. + /// - Parameter render: The flag indicating whether the progress bar has to render after the update. public func set(tasks newTasks: Int, render: Bool = true) { state.tasks = newTasks if render { @@ -92,7 +93,8 @@ extension ProgressBar { } /// Performs an addition to the current tasks. - /// - Parameter tasks: The tasks to add to the current tasks. + /// - Parameter delta: The tasks to add to the current tasks. + /// - Parameter render: The flag indicating whether the progress bar has to render after the update. public func add(tasks delta: Int, render: Bool = true) { _state.withLock { let newTasks = $0.tasks + delta @@ -104,7 +106,8 @@ extension ProgressBar { } /// Sets the total tasks. - /// - Parameter totalTasks: The total tasks to set. + /// - Parameter newTotalTasks: The total tasks to set. + /// - Parameter render: The flag indicating whether the progress bar has to render after the update. public func set(totalTasks newTotalTasks: Int, render: Bool = true) { state.totalTasks = newTotalTasks if render { @@ -113,7 +116,8 @@ extension ProgressBar { } /// Performs an addition to the total tasks. - /// - Parameter totalTasks: The tasks to add to the total tasks. + /// - Parameter delta: The tasks to add to the total tasks. + /// - Parameter render: The flag indicating whether the progress bar has to render after the update. public func add(totalTasks delta: Int, render: Bool = true) { _state.withLock { let totalTasks = $0.totalTasks ?? 0 @@ -126,7 +130,8 @@ extension ProgressBar { } /// Sets the items name. - /// - Parameter items: The current items to set. + /// - Parameter newItemsName: The current items to set. + /// - Parameter render: The flag indicating whether the progress bar has to render after the update. public func set(itemsName newItemsName: String, render: Bool = true) { state.itemsName = newItemsName if render { @@ -135,7 +140,7 @@ extension ProgressBar { } /// Sets the current items. - /// - Parameter items: The current items to set. + /// - Parameter newItems: The current items to set. public func set(items newItems: Int, render: Bool = true) { state.items = newItems if render { @@ -144,7 +149,8 @@ extension ProgressBar { } /// Performs an addition to the current items. - /// - Parameter items: The items to add to the current items. + /// - Parameter delta: The items to add to the current items. + /// - Parameter render: The flag indicating whether the progress bar has to render after the update. public func add(items delta: Int, render: Bool = true) { _state.withLock { let newItems = $0.items + delta @@ -156,7 +162,8 @@ extension ProgressBar { } /// Sets the total items. - /// - Parameter totalItems: The total items to set. + /// - Parameter newTotalItems: The total items to set. + /// - Parameter render: The flag indicating whether the progress bar has to render after the update. public func set(totalItems newTotalItems: Int, render: Bool = true) { state.totalItems = newTotalItems if render { @@ -165,7 +172,8 @@ extension ProgressBar { } /// Performs an addition to the total items. - /// - Parameter totalItems: The items to add to the total items. + /// - Parameter delta: The items to add to the total items. + /// - Parameter render: The flag indicating whether the progress bar has to render after the update. public func add(totalItems delta: Int, render: Bool = true) { _state.withLock { let totalItems = $0.totalItems ?? 0 @@ -178,7 +186,8 @@ extension ProgressBar { } /// Sets the current size. - /// - Parameter size: The current size to set. + /// - Parameter newSize: The current size to set. + /// - Parameter render: The flag indicating whether the progress bar has to render after the update. public func set(size newSize: Int64, render: Bool = true) { state.size = newSize if render { @@ -187,7 +196,8 @@ extension ProgressBar { } /// Performs an addition to the current size. - /// - Parameter size: The size to add to the current size. + /// - Parameter delta: The size to add to the current size. + /// - Parameter render: The flag indicating whether the progress bar has to render after the update. public func add(size delta: Int64, render: Bool = true) { _state.withLock { let newSize = $0.size + delta @@ -199,7 +209,8 @@ extension ProgressBar { } /// Sets the total size. - /// - Parameter totalSize: The total size to set. + /// - Parameter newTotalSize: The total size to set. + /// - Parameter render: The flag indicating whether the progress bar has to render after the update. public func set(totalSize newTotalSize: Int64, render: Bool = true) { state.totalSize = newTotalSize if render { @@ -208,7 +219,8 @@ extension ProgressBar { } /// Performs an addition to the total size. - /// - Parameter totalSize: The size to add to the total size. + /// - Parameter delta: The size to add to the total size. + /// - Parameter render: The flag indicating whether the progress bar has to render after the update. public func add(totalSize delta: Int64, render: Bool = true) { _state.withLock { let totalSize = $0.totalSize ?? 0 From 37c9732c4c729c10d494769b7c0d04bdfba6902f Mon Sep 17 00:00:00 2001 From: Danny Canter Date: Mon, 16 Jun 2025 23:11:58 -0700 Subject: [PATCH 35/45] makefile: Change build_bin_dir to be lazily evaluated (#221) The bin directory for placing built binaries to should be lazily evaluated, as we change BUILD_CONFIGURATION during `make release`. --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 2eba0398c..5e9cbde1b 100644 --- a/Makefile +++ b/Makefile @@ -21,7 +21,7 @@ export GIT_COMMIT := $(shell git rev-parse HEAD) SWIFT := "/usr/bin/swift" DESTDIR ?= /usr/local/ ROOT_DIR := $(shell git rev-parse --show-toplevel) -BUILD_BIN_DIR := $(shell $(SWIFT) build -c $(BUILD_CONFIGURATION) --show-bin-path) +BUILD_BIN_DIR = $(shell $(SWIFT) build -c $(BUILD_CONFIGURATION) --show-bin-path) STAGING_DIR := bin/$(BUILD_CONFIGURATION)/staging/ PKG_PATH := bin/$(BUILD_CONFIGURATION)/container-installer-unsigned.pkg DSYM_DIR := bin/$(BUILD_CONFIGURATION)/bundle/container-dSYM From 21cfebb475fb521002437397faad302fc7a57986 Mon Sep 17 00:00:00 2001 From: Ramsyana Date: Tue, 17 Jun 2025 14:19:46 +0800 Subject: [PATCH 36/45] Fix Race Condition in Container Removal (#130) (#218) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR resolves a race condition when removing a container immediately after stopping it, caused by the `stop` command returning before the container fully transitions to the stopped state (#130). **Changes:** - Enhanced `TestCLIRmRace.swift` with robust test logic and helper methods (`containerExists`, `safeRemove`). - Improved error handling to distinguish race conditions from successful removals. - Added exponential backoff retry logic for cleanup operations. - Updated `CLITest.swift` with missing `doRemove` method. - Fixed `BuilderStart.swift` to handle `.stopping` case. - Improved error messages with container ID for better debugging. **Testing:** - ✅ All tests pass (`make test`, `make integration`). - ✅ Verified on macOS 26. - ✅ Race condition test validates success and failure scenarios. - ✅ Code formatted (`make fmt`). Hopefully, this will pass the integration tests on GitHub. Signed-off-by: ramsyana <47033578+ramsyana@users.noreply.github.com> --- .../Containers/ContainersService.swift | 4 +- Sources/CLI/Builder/BuilderStart.swift | 5 + .../ContainerClient/Core/RuntimeStatus.swift | 2 + .../SandboxService.swift | 4 +- .../Build/CLIBuilderLifecycleTest.swift | 10 +- .../Containers/TestCLIRmRace.swift | 139 ++++++++++++++++++ Tests/CLITests/Utilities/CLITest.swift | 13 ++ 7 files changed, 169 insertions(+), 8 deletions(-) create mode 100644 Tests/CLITests/Subcommands/Containers/TestCLIRmRace.swift diff --git a/Sources/APIServer/Containers/ContainersService.swift b/Sources/APIServer/Containers/ContainersService.swift index 6970b9e1f..edadc3f00 100644 --- a/Sources/APIServer/Containers/ContainersService.swift +++ b/Sources/APIServer/Containers/ContainersService.swift @@ -209,10 +209,10 @@ actor ContainersService { switch item.state { case .alive(let client): let state = try await client.state() - if state.status == .running { + if state.status == .running || state.status == .stopping { throw ContainerizationError( .invalidState, - message: "container with ID \(id) is running" + message: "container \(id) is not yet stopped and can not be deleted" ) } try self._cleanup(id: id, item: item) diff --git a/Sources/CLI/Builder/BuilderStart.swift b/Sources/CLI/Builder/BuilderStart.swift index b1c98d3f0..76054e984 100644 --- a/Sources/CLI/Builder/BuilderStart.swift +++ b/Sources/CLI/Builder/BuilderStart.swift @@ -125,6 +125,11 @@ extension Application { return } try await existingContainer.delete() + case .stopping: + throw ContainerizationError( + .invalidState, + message: "builder is stopping, please wait until it is fully stopped before proceeding" + ) case .unknown: break } diff --git a/Sources/ContainerClient/Core/RuntimeStatus.swift b/Sources/ContainerClient/Core/RuntimeStatus.swift index 0d42496e4..12287dc92 100644 --- a/Sources/ContainerClient/Core/RuntimeStatus.swift +++ b/Sources/ContainerClient/Core/RuntimeStatus.swift @@ -24,4 +24,6 @@ public enum RuntimeStatus: String, CaseIterable, Sendable, Codable { case stopped /// The object is currently running. case running + /// The object is currently stopping. + case stopping } diff --git a/Sources/Services/ContainerSandboxService/SandboxService.swift b/Sources/Services/ContainerSandboxService/SandboxService.swift index 5a801c5fc..d7c0f5093 100644 --- a/Sources/Services/ContainerSandboxService/SandboxService.swift +++ b/Sources/Services/ContainerSandboxService/SandboxService.swift @@ -321,8 +321,10 @@ public actor SandboxService { var cs: ContainerSnapshot? switch state { - case .created, .stopped(_), .starting, .booted, .stopping: + case .created, .stopped(_), .starting, .booted: status = .stopped + case .stopping: + status = .stopping case .running: let ctr = try getContainer() diff --git a/Tests/CLITests/Subcommands/Build/CLIBuilderLifecycleTest.swift b/Tests/CLITests/Subcommands/Build/CLIBuilderLifecycleTest.swift index b66a3a6d2..dbee0cc42 100644 --- a/Tests/CLITests/Subcommands/Build/CLIBuilderLifecycleTest.swift +++ b/Tests/CLITests/Subcommands/Build/CLIBuilderLifecycleTest.swift @@ -24,14 +24,14 @@ extension TestCLIBuildBase { override init() throws {} @Test func testBuilderStartStopCommand() throws { #expect(throws: Never.self) { - try builderStart() - try waitForBuilderRunning() - let status = try getContainerStatus("buildkit") + try self.builderStart() + try self.waitForBuilderRunning() + let status = try self.getContainerStatus("buildkit") #expect(status == "running", "BuildKit container is not running") } #expect(throws: Never.self) { - try builderStop() - let status = try getContainerStatus("buildkit") + try self.builderStop() + let status = try self.getContainerStatus("buildkit") #expect(status == "stopped", "BuildKit container is not stopped") } } diff --git a/Tests/CLITests/Subcommands/Containers/TestCLIRmRace.swift b/Tests/CLITests/Subcommands/Containers/TestCLIRmRace.swift new file mode 100644 index 000000000..9ee3284bb --- /dev/null +++ b/Tests/CLITests/Subcommands/Containers/TestCLIRmRace.swift @@ -0,0 +1,139 @@ +//===----------------------------------------------------------------------===// +// Copyright © 2025 Apple Inc. and the container project authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//===----------------------------------------------------------------------===// + +// + +import Foundation +import Testing + +class TestCLIRmRaceCondition: CLITest { + + /// Helper method to check if a container exists + private func containerExists(_ name: String) -> Bool { + do { + _ = try getContainerStatus(name) + return true + } catch { + return false + } + } + + /// Safe container removal that handles already-removed containers gracefully + private func safeRemove(name: String, force: Bool = false) throws { + guard containerExists(name) else { + // Container already removed, nothing to do + return + } + try doRemove(name: name, force: force) + } + + @Test func testStopRmRace() async throws { + let name: String! = Test.current?.name.trimmingCharacters(in: ["(", ")"]) + + do { + // Create and start a container in detached mode that runs indefinitely + try doCreate(name: name, args: ["sleep", "infinity"]) + try doStart(name: name) + + // Wait for container to be running + try waitForContainerRunning(name) + + // Call doStop - this should return immediately without waiting + try doStop(name: name) + + // Immediately call doRemove and handle both possible outcomes: + // 1. Container removal succeeds immediately (race condition fixed) + // 2. Container removal fails because it's still stopping (race condition detected) + var raceConditionPrevented = false + var raceConditionDetected = false + + do { + try doRemove(name: name) + // Success: The race condition prevention is working perfectly! + // Container was removed cleanly without any race condition + raceConditionPrevented = true + } catch CLITest.CLIError.executionFailed(let message) { + if message.contains("is not yet stopped and can not be deleted") { + // Expected behavior: Race condition detected and prevented + raceConditionDetected = true + } else if message.contains("not found") || message.contains("failed to delete one or more containers") { + // Container was already removed by background cleanup - this is also success! + raceConditionPrevented = true + } else { + Issue.record("Unexpected error message: \(message)") + return + } + } catch { + Issue.record("Unexpected error type: \(error)") + return + } + + // Either outcome is acceptable - both indicate the race condition fix is working + #expect( + raceConditionPrevented || raceConditionDetected, + "Expected either immediate success (race prevented) or controlled failure (race detected)") + + // If the container was already removed, we're done + if raceConditionPrevented { + return + } + + // If we detected a race condition, wait for cleanup and retry removal + #expect(raceConditionDetected, "Should have detected race condition if we reach this point") + + // Give the background cleanup a moment to finish + try await Task.sleep(for: .seconds(2)) + + // Retry removal with exponential backoff for cleanup + var removeAttempts = 0 + let maxRemoveAttempts = 5 + let baseDelay = 1.0 // seconds + + while removeAttempts < maxRemoveAttempts { + do { + try safeRemove(name: name) + break + } catch CLITest.CLIError.executionFailed(let message) { + // If container doesn't exist, we're done + if message.contains("not found") { + break + } + + guard removeAttempts < maxRemoveAttempts - 1 else { + throw CLITest.CLIError.executionFailed("Failed to remove container after \(maxRemoveAttempts) attempts: \(message)") + } + + let delay = baseDelay * pow(2.0, Double(removeAttempts)) + try await Task.sleep(for: .seconds(delay)) + removeAttempts += 1 + } catch { + guard removeAttempts < maxRemoveAttempts - 1 else { + throw error + } + let delay = baseDelay * pow(2.0, Double(removeAttempts)) + try await Task.sleep(for: .seconds(delay)) + removeAttempts += 1 + } + } + + } catch { + Issue.record("failed to test stop-rm race condition: \(error)") + // Safe cleanup - only try to remove if container actually exists + try? safeRemove(name: name, force: true) + return + } + } +} diff --git a/Tests/CLITests/Utilities/CLITest.swift b/Tests/CLITests/Utilities/CLITest.swift index 950df6e3f..bf017d7dd 100644 --- a/Tests/CLITests/Utilities/CLITest.swift +++ b/Tests/CLITests/Utilities/CLITest.swift @@ -367,4 +367,17 @@ class CLITest { throw CLIError.executionFailed("command failed: \(error)") } } + + func doRemove(name: String, force: Bool = false) throws { + var args = ["delete"] + if force { + args.append("--force") + } + args.append(name) + + let (_, error, status) = try run(arguments: args) + if status != 0 { + throw CLIError.executionFailed("command failed: \(error)") + } + } } From a262d8fc6a5abf133cf196b4874b033a414c24ef Mon Sep 17 00:00:00 2001 From: Spencer Heywood <18178614+heywoodlh@users.noreply.github.com> Date: Thu, 19 Jun 2025 01:12:57 -0600 Subject: [PATCH 37/45] provide suggestion if xpc 'Connection invalid' error encountered (#179) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes https://github.com/apple/container/issues/80 Adds the following help message if you try to run `container` against a host that hasn't started the container system: ``` ❯ /usr/local/bin/container list Error: internalError: "failed to list containers" (cause: "interrupted: "Connection invalid: ensure container system has been started with `container system start`"") ❯ /usr/local/bin/container run -it --rm docker.io/alpine Error: interrupted: "Connection invalid: ensure container system has been started with `container system start`" ``` --- README.md | 6 ++++++ Sources/CLI/Application.swift | 9 ++++++++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 56c3da31b..179b9b32e 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,12 @@ Download the latest signed installer package for `container` from the [GitHub re To install the tool, double click the package file and follow the instructions. Enter your administrator password when prompted, to give the installer permission to place the installed files under `/usr/local`. +Start the system service with: + +``` +container system start +``` + ### Uninstall Use the `uninstall-container.sh` script to remove `container` from your system. To remove your user data along with the tool, run: diff --git a/Sources/CLI/Application.swift b/Sources/CLI/Application.swift index ea3e7a94e..cd7c22fb2 100644 --- a/Sources/CLI/Application.swift +++ b/Sources/CLI/Application.swift @@ -21,6 +21,7 @@ import CVersion import ContainerClient import ContainerLog import ContainerPlugin +import ContainerizationError import ContainerizationOS import Foundation import Logging @@ -176,7 +177,13 @@ struct Application: AsyncParsableCommand { Self.printModifiedHelpText() return } - Application.exit(withError: error) + let errorAsString: String = String(describing: error) + if errorAsString.contains("XPC connection error") { + let modifiedError = ContainerizationError(.interrupted, message: "\(error)\nEnsure container system service has been started with `container system start`.") + Application.exit(withError: modifiedError) + } else { + Application.exit(withError: error) + } } } From a69ed784847670869ec6f9d1446b48d8c05dc3e5 Mon Sep 17 00:00:00 2001 From: Renee Chang <136089488+Reneechang17@users.noreply.github.com> Date: Fri, 20 Jun 2025 09:00:47 -0700 Subject: [PATCH 38/45] Add socket publishing functionality (#236) Signed-off-by: renee chang --- .../Core/ContainerConfiguration.swift | 40 +++++++++ .../ContainerClient/Core/PublishSocket.swift | 40 +++++++++ Sources/ContainerClient/Flags.swift | 3 + Sources/ContainerClient/Parser.swift | 90 +++++++++++++++++++ Sources/ContainerClient/Utility.swift | 4 + .../SandboxService.swift | 10 +++ 6 files changed, 187 insertions(+) create mode 100644 Sources/ContainerClient/Core/PublishSocket.swift diff --git a/Sources/ContainerClient/Core/ContainerConfiguration.swift b/Sources/ContainerClient/Core/ContainerConfiguration.swift index 86d823281..bb73d8f4c 100644 --- a/Sources/ContainerClient/Core/ContainerConfiguration.swift +++ b/Sources/ContainerClient/Core/ContainerConfiguration.swift @@ -23,6 +23,8 @@ public struct ContainerConfiguration: Sendable, Codable { public var image: ImageDescription /// External mounts to add to the container. public var mounts: [Filesystem] = [] + /// Sockets to publish from container to host. + public var publishedSockets: [PublishSocket] = [] /// Key/Value labels for the container. public var labels: [String: String] = [:] /// System controls for the container. @@ -44,6 +46,44 @@ public struct ContainerConfiguration: Sendable, Codable { /// Name of the runtime that supports the container public var runtimeHandler: String = "container-runtime-linux" + enum CodingKeys: String, CodingKey { + case id + case image + case mounts + case publishedSockets + case labels + case sysctls + case networks + case dns + case rosetta + case hostname + case initProcess + case platform + case resources + case runtimeHandler + } + + /// Create a configuration from the supplied Decoder, initializing missing + /// values where possible to reasonable defaults. + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + id = try container.decode(String.self, forKey: .id) + image = try container.decode(ImageDescription.self, forKey: .image) + mounts = try container.decodeIfPresent([Filesystem].self, forKey: .mounts) ?? [] + publishedSockets = try container.decodeIfPresent([PublishSocket].self, forKey: .publishedSockets) ?? [] + labels = try container.decodeIfPresent([String: String].self, forKey: .labels) ?? [:] + sysctls = try container.decodeIfPresent([String: String].self, forKey: .sysctls) ?? [:] + networks = try container.decodeIfPresent([String].self, forKey: .networks) ?? [] + dns = try container.decodeIfPresent(DNSConfiguration.self, forKey: .dns) + rosetta = try container.decodeIfPresent(Bool.self, forKey: .rosetta) ?? false + hostname = try container.decodeIfPresent(String.self, forKey: .hostname) + initProcess = try container.decode(ProcessConfiguration.self, forKey: .initProcess) + platform = try container.decodeIfPresent(ContainerizationOCI.Platform.self, forKey: .platform) ?? .current + resources = try container.decodeIfPresent(Resources.self, forKey: .resources) ?? .init() + runtimeHandler = try container.decodeIfPresent(String.self, forKey: .runtimeHandler) ?? "container-runtime-linux" + } + public struct DNSConfiguration: Sendable, Codable { public static let defaultNameservers = ["1.1.1.1"] diff --git a/Sources/ContainerClient/Core/PublishSocket.swift b/Sources/ContainerClient/Core/PublishSocket.swift new file mode 100644 index 000000000..cd6fcf148 --- /dev/null +++ b/Sources/ContainerClient/Core/PublishSocket.swift @@ -0,0 +1,40 @@ +//===----------------------------------------------------------------------===// +// Copyright © 2025 Apple Inc. and the container project authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//===----------------------------------------------------------------------===// + +import Foundation +import SystemPackage + +/// Represents a socket that should be published from container to host. +public struct PublishSocket: Sendable, Codable { + /// The path to the socket in the container. + public var containerPath: URL + + /// The path where the socket should appear on the host. + public var hostPath: URL + + /// File permissions for the socket on the host. + public var permissions: FilePermissions? + + public init( + containerPath: URL, + hostPath: URL, + permissions: FilePermissions? = nil + ) { + self.containerPath = containerPath + self.hostPath = hostPath + self.permissions = permissions + } +} diff --git a/Sources/ContainerClient/Flags.swift b/Sources/ContainerClient/Flags.swift index 5bbbfaabd..d0924e5ac 100644 --- a/Sources/ContainerClient/Flags.swift +++ b/Sources/ContainerClient/Flags.swift @@ -92,6 +92,9 @@ public struct Flags { @Option(name: .customLong("mount"), help: "Add a mount to the container (type=<>,source=<>,target=<>,readonly)") public var mounts: [String] = [] + @Option(name: .customLong("publish-socket"), help: "Publish a socket from container to host (format: host_path:container_path)") + public var publishSockets: [String] = [] + @Option(name: .customLong("tmpfs"), help: "Add a tmpfs mount to the container at the given path") public var tmpFs: [String] = [] diff --git a/Sources/ContainerClient/Parser.swift b/Sources/ContainerClient/Parser.swift index 65286b651..b79fdad56 100644 --- a/Sources/ContainerClient/Parser.swift +++ b/Sources/ContainerClient/Parser.swift @@ -396,4 +396,94 @@ public struct Parser { throw ContainerizationError(.invalidArgument, message: "mount destination cannot be empty") } } + + // Parse --publish-socket arguments into PublishSocket objects + // Format: "host_path:container_path" (e.g., "/tmp/docker.sock:/var/run/docker.sock") + // + // - Parameter rawPublishSockets: Array of socket specifications + // - Returns: Array of PublishSocket objects + // - Throws: ContainerizationError if parsing fails + static func publishSockets(_ rawPublishSockets: [String]) throws -> [PublishSocket] { + var sockets: [PublishSocket] = [] + + // Process each raw socket string + for socket in rawPublishSockets { + let parsedSocket = try Parser.publishSocket(socket) + sockets.append(parsedSocket) + } + return sockets + } + + // Parse a single --publish-socket argument and validate paths + // Format: "host_path:container_path" -> PublishSocket + private static func publishSocket(_ socket: String) throws -> PublishSocket { + // Split by colon to two parts: [host_path, container_path] + let parts = socket.split(separator: ":") + + switch parts.count { + case 2: + // Extract host and container paths + let hostPath = String(parts[0]) + let containerPath = String(parts[1]) + + // Validate paths are not empty + if hostPath.isEmpty { + throw ContainerizationError( + .invalidArgument, message: "host socket path cannot be empty") + } + if containerPath.isEmpty { + throw ContainerizationError( + .invalidArgument, message: "container socket path cannot be empty") + } + + // Ensure container path must start with / + if !containerPath.hasPrefix("/") { + throw ContainerizationError( + .invalidArgument, + message: "container socket path must be absolute: \(containerPath)") + } + + // Convert host path to absolute path for consistency + let hostURL = URL(fileURLWithPath: hostPath) + let absoluteHostPath = hostURL.absoluteURL.path + + // Check if host socket already exists and might be in use + if FileManager.default.fileExists(atPath: absoluteHostPath) { + do { + let attrs = try FileManager.default.attributesOfItem(atPath: absoluteHostPath) + if let fileType = attrs[.type] as? FileAttributeType, fileType == .typeSocket { + throw ContainerizationError( + .invalidArgument, + message: "host socket \(absoluteHostPath) already exists and may be in use") + } + // If it exists but is not a socket, we can remove it and create socket + try FileManager.default.removeItem(atPath: absoluteHostPath) + } catch let error as ContainerizationError { + throw error + } catch { + // For other file system errors, continue with creation + } + } + + // Create host directory if it doesn't exist + let hostDir = hostURL.deletingLastPathComponent() + if !FileManager.default.fileExists(atPath: hostDir.path) { + try FileManager.default.createDirectory( + at: hostDir, withIntermediateDirectories: true) + } + + // Create and return PublishSocket object with validated paths + return PublishSocket( + containerPath: URL(fileURLWithPath: containerPath), + hostPath: URL(fileURLWithPath: absoluteHostPath), + permissions: nil + ) + + default: + throw ContainerizationError( + .invalidArgument, + message: + "invalid publish-socket format \(socket). Expected: host_path:container_path") + } + } } diff --git a/Sources/ContainerClient/Utility.swift b/Sources/ContainerClient/Utility.swift index 1b8bfc525..91158e88d 100644 --- a/Sources/ContainerClient/Utility.swift +++ b/Sources/ContainerClient/Utility.swift @@ -182,6 +182,10 @@ public struct Utility { config.labels = try Parser.labels(management.labels) + // Parse --publish-socket arguments and add to container configuration + // to enable socket forwarding from container to host. + config.publishedSockets = try Parser.publishSockets(management.publishSockets) + return (config, kernel) } diff --git a/Sources/Services/ContainerSandboxService/SandboxService.swift b/Sources/Services/ContainerSandboxService/SandboxService.swift index d7c0f5093..3e63fe34d 100644 --- a/Sources/Services/ContainerSandboxService/SandboxService.swift +++ b/Sources/Services/ContainerSandboxService/SandboxService.swift @@ -620,6 +620,16 @@ public actor SandboxService { } } + for publishedSocket in config.publishedSockets { + let socketConfig = UnixSocketConfiguration( + source: publishedSocket.containerPath, + destination: publishedSocket.hostPath, + permissions: publishedSocket.permissions, + direction: .outOf + ) + container.sockets.append(socketConfig) + } + container.hostname = config.hostname ?? config.id if let dns = config.dns { From 8e892be2b014d4cb58961d3e5ff9b7dfd1c38863 Mon Sep 17 00:00:00 2001 From: Joseph Heck Date: Fri, 20 Jun 2025 15:28:17 -0700 Subject: [PATCH 39/45] limit build and test for runners to Apple repository (#228) constrains build to run on apple/container fixes #209 --- .github/workflows/common.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/common.yml b/.github/workflows/common.yml index cc00da1fd..9c6aac1db 100644 --- a/.github/workflows/common.yml +++ b/.github/workflows/common.yml @@ -11,6 +11,7 @@ on: jobs: buildAndTest: name: Build and test the project + if: github.repository == 'apple/container' timeout-minutes: 60 runs-on: [self-hosted, macos, sequoia, ARM64] permissions: From 65e173333bc8b99ee3fb52fd87fbcc669e80448e Mon Sep 17 00:00:00 2001 From: Dmitry Kovba Date: Mon, 23 Jun 2025 10:19:44 -0700 Subject: [PATCH 40/45] Redirect to the documentation from the homepage (#245) This PR fixes the homepage at https://apple.github.io/container/ --- scripts/make-docs.sh | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/scripts/make-docs.sh b/scripts/make-docs.sh index 121ae0f6e..2e07c288f 100755 --- a/scripts/make-docs.sh +++ b/scripts/make-docs.sh @@ -40,3 +40,15 @@ fi /usr/bin/swift package ${opts[@]} echo '{}' > "$1/theme-settings.json" + +cat > "$1/index.html" <<'EOF' + + + + + + +

If you are not redirected automatically, click here.

+ + +EOF From 90083956c2d4271475395793575cd10c2d83a702 Mon Sep 17 00:00:00 2001 From: John Spurlock Date: Tue, 24 Jun 2025 12:08:16 -0500 Subject: [PATCH 41/45] Fix typo in technical-overview.md (#253) --- docs/technical-overview.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/technical-overview.md b/docs/technical-overview.md index 897780e61..5e9437830 100644 --- a/docs/technical-overview.md +++ b/docs/technical-overview.md @@ -80,7 +80,7 @@ In macOS 15, limitations in the vmnet framework mean that the container network Normally, vmnet creates the container network using the CIDR address 192.168.64.1/24, and on macOS 15, `container` defaults to using this CIDR address in the network helper. To diagnose and resolve issues stemming from a subnet address mismatch between vmnet and the network helper: - Before creating the first container, scan the output of the command `ifconfig` for a bridge interface named similarly to `bridge100`. -- After creating the first container, run `ifconfig` again, and locate the new bridge interface to determine container the subnet address. +- After creating the first container, run `ifconfig` again, and locate the new bridge interface to determine the container subnet address. - Run `container ls` to check the IP address given to the container by the network helper. If the address corresponds to a different network: - Run `container system stop` to terminate the services for `container`. - Using the macOS `defaults` command, update the default subnet value used by the network helper process. For example, if the bridge address shown by `ifconfig` is 192.168.66.1, run: From aa71809da6e7db5aec39635bf631aa391bd3737d Mon Sep 17 00:00:00 2001 From: Dmitry Kovba Date: Tue, 24 Jun 2025 10:45:21 -0700 Subject: [PATCH 42/45] Remove `@unchecked Sendable` (#250) This PR removes `@unchecked Sendable` from the tests. --- .../Subcommands/Build/CLIRunBase.swift | 19 +++++++------------ .../Subcommands/Build/TestCLITermIO.swift | 3 +-- 2 files changed, 8 insertions(+), 14 deletions(-) diff --git a/Tests/CLITests/Subcommands/Build/CLIRunBase.swift b/Tests/CLITests/Subcommands/Build/CLIRunBase.swift index c8abb1fd6..82ea0132e 100644 --- a/Tests/CLITests/Subcommands/Build/CLIRunBase.swift +++ b/Tests/CLITests/Subcommands/Build/CLIRunBase.swift @@ -20,8 +20,7 @@ import ContainerizationOS import Foundation import Testing -// This test class is not thread safe -class TestCLIRunBase: CLITest, @unchecked Sendable { +class TestCLIRunBase: CLITest { var terminal: Terminal! var containerName: String = UUID().uuidString @@ -62,7 +61,12 @@ class TestCLIRunBase: CLITest, @unchecked Sendable { func containerRun(stdin: [String], findMessage: String) async throws -> Bool { let stdout = FileHandle(fileDescriptor: terminal.handle.fileDescriptor, closeOnDealloc: false) let stdoutListenTask = Task { - try await findStdoutOutput(stdout: stdout, findMessage: findMessage) + for try await line in stdout.bytes.lines { + if line.contains(findMessage) && !line.contains("echo") { + return true + } + } + return false } let timeoutTask = Task { @@ -82,15 +86,6 @@ class TestCLIRunBase: CLITest, @unchecked Sendable { } } - func findStdoutOutput(stdout: FileHandle, findMessage: String) async throws -> Bool { - for try await line in stdout.bytes.lines { - if line.contains(findMessage) && !line.contains("echo") { - return true - } - } - return false - } - func exec(commands: [String]) throws { let stdin = FileHandle(fileDescriptor: terminal.handle.fileDescriptor, closeOnDealloc: false) try commands.forEach { cmd in diff --git a/Tests/CLITests/Subcommands/Build/TestCLITermIO.swift b/Tests/CLITests/Subcommands/Build/TestCLITermIO.swift index 26bc68d02..35519f677 100644 --- a/Tests/CLITests/Subcommands/Build/TestCLITermIO.swift +++ b/Tests/CLITests/Subcommands/Build/TestCLITermIO.swift @@ -21,8 +21,7 @@ import Foundation import Testing extension TestCLIRunBase { - // This test class is NOT thread safe - class TestCLITermIO: TestCLIRunBase, @unchecked Sendable { + class TestCLITermIO: TestCLIRunBase { override var ContainerImage: String { "ghcr.io/linuxcontainers/alpine:3.20" } From 5d36134b961ddba6ccde4cb51a92c6287b49e1ee Mon Sep 17 00:00:00 2001 From: Dmitry Kovba Date: Tue, 24 Jun 2025 12:46:50 -0700 Subject: [PATCH 43/45] Regenerate documentation on `make docs` (#246) This PR allows using `make docs` without manually removing the existing `_site` folder. --- Makefile | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/Makefile b/Makefile index 5e9cbde1b..114b39979 100644 --- a/Makefile +++ b/Makefile @@ -182,12 +182,10 @@ serve-docs: @python3 -m http.server --bind 127.0.0.1 --directory ./_serve .PHONY: docs -docs: _site - -_site: +docs: @echo Updating API documentation... - rm -rf $@ - @scripts/make-docs.sh $@ container + @rm -rf _site + @scripts/make-docs.sh _site container .PHONY: cleancontent cleancontent: From 4a6a1f15d83f1584e97a5600fb49d4e2d9eff3fa Mon Sep 17 00:00:00 2001 From: Kathryn Baldauf Date: Tue, 24 Jun 2025 13:25:58 -0700 Subject: [PATCH 44/45] Add test that we replace meta args in builder correctly (#255) Depends on https://github.com/apple/container-builder-shim/pull/24 Related to https://github.com/apple/container/issues/252 --------- Signed-off-by: Kathryn Baldauf --- Package.swift | 2 +- Sources/ContainerBuild/Builder.grpc.swift | 8 ++++---- .../CLITests/Subcommands/Build/CLIBuilderTest.swift | 13 +++++++++++++ 3 files changed, 18 insertions(+), 5 deletions(-) diff --git a/Package.swift b/Package.swift index 8fa4ba087..cc2e0376a 100644 --- a/Package.swift +++ b/Package.swift @@ -32,7 +32,7 @@ if let path = ProcessInfo.processInfo.environment["CONTAINERIZATION_PATH"] { let releaseVersion = ProcessInfo.processInfo.environment["RELEASE_VERSION"] ?? "0.0.0" let gitCommit = ProcessInfo.processInfo.environment["GIT_COMMIT"] ?? "unspecified" -let builderShimVersion = "0.2.0" +let builderShimVersion = "0.2.1" let package = Package( name: "container", diff --git a/Sources/ContainerBuild/Builder.grpc.swift b/Sources/ContainerBuild/Builder.grpc.swift index e742b9557..33d5eee26 100644 --- a/Sources/ContainerBuild/Builder.grpc.swift +++ b/Sources/ContainerBuild/Builder.grpc.swift @@ -110,7 +110,7 @@ import SwiftProtobuf /// /// /// NOTE: the client should close the send stream once it has finished -/// receiving the build output or abadon the current build due to error. +/// receiving the build output or abandon the current build due to error. /// Server should keep the stream open until it receives the EOF that client /// has closed the stream, which the server should then close its send stream. /// @@ -340,7 +340,7 @@ public struct Com_Apple_Container_Build_V1_BuilderNIOClient: Com_Apple_Container /// /// /// NOTE: the client should close the send stream once it has finished -/// receiving the build output or abadon the current build due to error. +/// receiving the build output or abandon the current build due to error. /// Server should keep the stream open until it receives the EOF that client /// has closed the stream, which the server should then close its send stream. @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) @@ -603,7 +603,7 @@ public enum Com_Apple_Container_Build_V1_BuilderClientMetadata { /// /// /// NOTE: the client should close the send stream once it has finished -/// receiving the build output or abadon the current build due to error. +/// receiving the build output or abandon the current build due to error. /// Server should keep the stream open until it receives the EOF that client /// has closed the stream, which the server should then close its send stream. /// @@ -750,7 +750,7 @@ extension Com_Apple_Container_Build_V1_BuilderProvider { /// /// /// NOTE: the client should close the send stream once it has finished -/// receiving the build output or abadon the current build due to error. +/// receiving the build output or abandon the current build due to error. /// Server should keep the stream open until it receives the EOF that client /// has closed the stream, which the server should then close its send stream. /// diff --git a/Tests/CLITests/Subcommands/Build/CLIBuilderTest.swift b/Tests/CLITests/Subcommands/Build/CLIBuilderTest.swift index 26247192b..b557699f9 100644 --- a/Tests/CLITests/Subcommands/Build/CLIBuilderTest.swift +++ b/Tests/CLITests/Subcommands/Build/CLIBuilderTest.swift @@ -113,6 +113,19 @@ extension TestCLIBuildBase { #expect(try self.inspectImage(imageName) == imageName, "expected to have successfully built \(imageName)") } + @Test func testBuildArg() throws { + let tempDir: URL = try createTempDir() + let dockerfile: String = + """ + ARG TAG=unknown + FROM ghcr.io/linuxcontainers/alpine:${TAG} + """ + try createContext(tempDir: tempDir, dockerfile: dockerfile) + let imageName: String = "registry.local/build-arg:\(UUID().uuidString)" + try self.build(tag: imageName, tempDir: tempDir, args: ["TAG=3.20"]) + #expect(try self.inspectImage(imageName) == imageName, "expected to have successfully built \(imageName)") + } + @Test func testBuildNetworkAccess() throws { let tempDir: URL = try createTempDir() let dockerfile: String = From c7c88c2e342a57b5a4104d0c36ea52926fe95f68 Mon Sep 17 00:00:00 2001 From: Sidhartha Mani Date: Wed, 25 Jun 2025 13:51:51 -0700 Subject: [PATCH 45/45] [Build] Do not use unbounded DispatchIO readers for tar tranfers (#257) - Addresses https://github.com/apple/container/issues/166 - Memory utilization explodes since there is no mechanism for backpressure - Using a synchronous buffered reader seem to provide similar performance without the memory explosion issue - 4MB buffer seems to provide the best results | Metric | 1MB Buffer | 4MB Buffer | Unbounded Zero-Copy | |--------------------------|------------|------------|---------------------| | Build Time | 149.33s | 138.57s | 139.79s | | Max RAM Used | 2.16 GB | 3.02 GB | 3.52 GB | | Peak Memory Footprint | 8.30 GB | 8.17 GB | 10.21 GB | | Page Reclaims | 1,085,559 | 1,039,677 | 1,619,943 | | Page Faults | 115 | 148 | 143 | | CPU Usage (User+Sys) | 53.71s | 53.12s | 60.44s | --- Sources/ContainerBuild/BuildFSSync.swift | 2 +- Sources/ContainerBuild/URL+Extensions.swift | 143 ++++++++++++++++++++ 2 files changed, 144 insertions(+), 1 deletion(-) diff --git a/Sources/ContainerBuild/BuildFSSync.swift b/Sources/ContainerBuild/BuildFSSync.swift index a41c629fc..9f4e91129 100644 --- a/Sources/ContainerBuild/BuildFSSync.swift +++ b/Sources/ContainerBuild/BuildFSSync.swift @@ -229,7 +229,7 @@ actor BuildFSSync: BuildPipelineHandler { pathInArchive: URL(fileURLWithPath: rel)) } - for await chunk in try tarURL.zeroCopyReader() { + for try await chunk in try tarURL.bufferedCopyReader() { let part = BuildTransfer( id: packet.id, source: tarURL.path, diff --git a/Sources/ContainerBuild/URL+Extensions.swift b/Sources/ContainerBuild/URL+Extensions.swift index 75b736de8..a00b13ef6 100644 --- a/Sources/ContainerBuild/URL+Extensions.swift +++ b/Sources/ContainerBuild/URL+Extensions.swift @@ -138,4 +138,147 @@ extension URL { } } } + + func bufferedCopyReader(chunkSize: Int = 4 * 1024 * 1024) throws -> BufferedCopyReader { + try BufferedCopyReader(url: self, chunkSize: chunkSize) + } +} + +/// A synchronous buffered reader that reads one chunk at a time from a file +/// Uses a configurable buffer size (default 4MB) and only reads when nextChunk() is called +/// Implements AsyncSequence for use with `for await` loops +public final class BufferedCopyReader: AsyncSequence { + public typealias Element = Data + public typealias AsyncIterator = BufferedCopyReaderIterator + + private let inputStream: InputStream + private let chunkSize: Int + private var isFinished: Bool = false + private let reusableBuffer: UnsafeMutablePointer + + /// Initialize a buffered copy reader for the given URL + /// - Parameters: + /// - url: The file URL to read from + /// - chunkSize: Size of each chunk to read (default: 4MB) + public init(url: URL, chunkSize: Int = 4 * 1024 * 1024) throws { + guard let stream = InputStream(url: url) else { + throw CocoaError(.fileReadNoSuchFile) + } + self.inputStream = stream + self.chunkSize = chunkSize + self.reusableBuffer = UnsafeMutablePointer.allocate(capacity: chunkSize) + self.inputStream.open() + } + + deinit { + inputStream.close() + reusableBuffer.deallocate() + } + + /// Create an async iterator for this sequence + public func makeAsyncIterator() -> BufferedCopyReaderIterator { + BufferedCopyReaderIterator(reader: self) + } + + /// Read the next chunk of data from the file + /// - Returns: Data chunk, or nil if end of file reached + /// - Throws: Any file reading errors + public func nextChunk() throws -> Data? { + guard !isFinished else { return nil } + + // Read directly into our reusable buffer + let bytesRead = inputStream.read(reusableBuffer, maxLength: chunkSize) + + // Check for errors + if bytesRead < 0 { + if let error = inputStream.streamError { + throw error + } + throw CocoaError(.fileReadUnknown) + } + + // If we read no data, we've reached the end + if bytesRead == 0 { + isFinished = true + return nil + } + + // If we read less than the chunk size, this is the last chunk + if bytesRead < chunkSize { + isFinished = true + } + + // Create Data object only with the bytes actually read + return Data(bytes: reusableBuffer, count: bytesRead) + } + + /// Check if the reader has finished reading the file + public var hasFinished: Bool { + isFinished + } + + /// Reset the reader to the beginning of the file + /// Note: InputStream doesn't support seeking, so this recreates the stream + /// - Throws: Any file opening errors + public func reset() throws { + inputStream.close() + // Note: InputStream doesn't provide a way to get the original URL, + // so reset functionality is limited. Consider removing this method + // or storing the original URL if reset is needed. + throw CocoaError( + .fileReadUnsupportedScheme, + userInfo: [ + NSLocalizedDescriptionKey: "Reset not supported with InputStream-based implementation" + ]) + } + + /// Get the current file offset + /// Note: InputStream doesn't provide offset information + /// - Returns: Current position in the file + /// - Throws: Unsupported operation error + public func currentOffset() throws -> UInt64 { + throw CocoaError( + .fileReadUnsupportedScheme, + userInfo: [ + NSLocalizedDescriptionKey: "Offset tracking not supported with InputStream-based implementation" + ]) + } + + /// Seek to a specific offset in the file + /// Note: InputStream doesn't support seeking + /// - Parameter offset: The byte offset to seek to + /// - Throws: Unsupported operation error + public func seek(to offset: UInt64) throws { + throw CocoaError( + .fileReadUnsupportedScheme, + userInfo: [ + NSLocalizedDescriptionKey: "Seeking not supported with InputStream-based implementation" + ]) + } + + /// Close the input stream explicitly (called automatically in deinit) + public func close() { + inputStream.close() + isFinished = true + } +} + +/// AsyncIteratorProtocol implementation for BufferedCopyReader +public struct BufferedCopyReaderIterator: AsyncIteratorProtocol { + public typealias Element = Data + + private let reader: BufferedCopyReader + + init(reader: BufferedCopyReader) { + self.reader = reader + } + + /// Get the next chunk of data asynchronously + /// - Returns: Next data chunk, or nil when finished + /// - Throws: Any file reading errors + public mutating func next() async throws -> Data? { + // Yield control to allow other tasks to run, then read synchronously + await Task.yield() + return try reader.nextChunk() + } }