├── .github ├── ISSUE_TEMPLATE.md ├── PULL_REQUEST_TEMPLATE.md ├── release.yml └── workflows │ ├── endtoend_tests.yml │ ├── integration_tests.yml │ ├── interop_tests.yml │ ├── main.yml │ └── pull_request.yml ├── .gitignore ├── .licenseignore ├── .mailmap ├── .spi.yml ├── .swift-format ├── .swiftformatignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── CONTRIBUTORS.txt ├── Examples ├── HelloWorldHummingbird │ ├── .gitignore │ ├── Package.swift │ └── Sources │ │ ├── App.swift │ │ └── Application+build.swift ├── HelloWorldVapor │ ├── .gitignore │ ├── Package.swift │ └── Sources │ │ ├── configure.swift │ │ ├── entrypoint.swift │ │ └── routes.swift └── HelloWorldWithResources │ ├── .gitignore │ ├── Package.swift │ └── Sources │ ├── App.swift │ ├── Application+build.swift │ └── resources │ ├── happy-cat-face.jpg │ ├── slightly-smiling-face.jpg │ └── smiling-face-with-sunglasses.jpg ├── LICENSE.txt ├── NOTICE.txt ├── Package.swift ├── Plugins └── ContainerImageBuilder │ ├── Pipe+lines.swift │ ├── main.swift │ └── runner.swift ├── README.md ├── SECURITY.md ├── Sources ├── ContainerRegistry │ ├── AuthHandler.swift │ ├── Blobs.swift │ ├── CheckAPI.swift │ ├── HTTPClient.swift │ ├── ImageManifest+Digest.swift │ ├── ImageReference.swift │ ├── Manifests.swift │ ├── RegistryClient+ImageConfiguration.swift │ ├── RegistryClient.swift │ ├── Schema.swift │ └── Tags.swift ├── Tar │ └── tar.swift ├── containertool │ ├── ELFDetect.swift │ ├── Extensions │ │ ├── Archive+appending.swift │ │ ├── ELF+containerArchitecture.swift │ │ ├── Errors+CustomStringConvertible.swift │ │ ├── NetrcError+CustomStringConvertible.swift │ │ ├── RegistryClient+CopyBlobs.swift │ │ └── RegistryClient+Layers.swift │ ├── Logging.swift │ ├── containertool.swift │ └── gzip.swift └── swift-container-plugin │ ├── Documentation.docc │ ├── Adding-the-plugin-to-your-project.md │ ├── Info.plist │ ├── Swift-Container-Plugin.md │ ├── _Resources │ │ └── swift-container-plugin-flow-diagram.png │ ├── authentication.md │ ├── build-container-image.md │ ├── build.md │ ├── requirements.md │ └── run.md │ ├── Empty.swift │ └── README.md ├── Tests ├── ContainerRegistryTests │ ├── AuthTests.swift │ ├── ImageReferenceTests.swift │ ├── Resources │ │ ├── netrc.basic │ │ ├── netrc.default │ │ ├── netrc.empty │ │ └── netrc.invaliddefault │ └── SmokeTests.swift ├── TarTests │ ├── TarInteropTests.swift │ └── TarUnitTests.swift └── containertoolTests │ ├── ELFDetectTests.swift │ └── ZlibTests.swift ├── Vendor └── github.com │ └── apple │ ├── swift-nio-extras │ ├── LICENSE.txt │ └── Sources │ │ └── CNIOExtrasZlib │ │ ├── empty.c │ │ └── include │ │ └── CNIOExtrasZlib.h │ └── swift-package-manager │ ├── LICENSE.txt │ └── Sources │ └── Basics │ ├── AuthorizationProvider.swift │ └── Netrc.swift └── scripts ├── generate-contributors-list.sh ├── run-integration-tests.sh ├── test-containertool-elf-detection.sh ├── test-containertool-resources.sh └── test-plugin-output-streaming.sh /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### Description 2 | 3 | _[Explain what causes the problem you're reporting.]_ 4 | 5 | ### Reproduction 6 | 7 | _[Please provide inputs to help us reproduce the problem. Try to reduce the test case to the smallest amount of code possible which still reproduces the problem.]_ 8 | 9 | ### Package versions 10 | 11 | _[Provide the version of the Swift Container Plugin you are using when encountering the problem.]_ 12 | 13 | ### Expected behavior 14 | 15 | _[Describe what you expect to happen.]_ 16 | 17 | ### Environment 18 | 19 | _[Provide the Swift version, tag, or revision you are using.]_ 20 | 21 | ### Additional information 22 | 23 | _[Provide any extra information which could help others understand or work around the problem.]_ 24 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | Motivation 2 | ---------- 3 | 4 | _[Explain here the context, and why you're making that change. What is the problem you're trying to solve.]_ 5 | 6 | Modifications 7 | ------------- 8 | 9 | _[Describe the modifications you've made.]_ 10 | 11 | Result 12 | ------ 13 | 14 | _[After your change, what will change.]_ 15 | 16 | Test Plan 17 | --------- 18 | 19 | _[Describe the steps you took, or will take, to qualify the change - such as adjusting tests and manual testing.]_ 20 | -------------------------------------------------------------------------------- /.github/release.yml: -------------------------------------------------------------------------------- 1 | changelog: 2 | categories: 3 | - title: SemVer Major 4 | labels: 5 | - semver/major 6 | - title: SemVer Minor 7 | labels: 8 | - semver/minor 9 | - title: SemVer Patch 10 | labels: 11 | - semver/patch 12 | - title: Other Changes 13 | labels: 14 | - semver/none 15 | - "*" 16 | -------------------------------------------------------------------------------- /.github/workflows/endtoend_tests.yml: -------------------------------------------------------------------------------- 1 | name: End to end tests 2 | 3 | on: 4 | workflow_call: 5 | # inputs: 6 | # example: 7 | # required: true 8 | # type: string 9 | 10 | jobs: 11 | endtoend-tests: 12 | name: End to end tests 13 | runs-on: ubuntu-latest 14 | services: 15 | registry: 16 | image: registry:2 17 | ports: 18 | - 5000:5000 19 | strategy: 20 | matrix: 21 | example: 22 | - Examples/HelloWorldVapor 23 | - Examples/HelloWorldHummingbird 24 | - Examples/HelloWorldWithResources 25 | steps: 26 | - name: Checkout repository 27 | uses: actions/checkout@v4 28 | with: 29 | persist-credentials: false 30 | 31 | - name: Mark the workspace as safe 32 | # https://github.com/actions/checkout/issues/766 33 | run: git config --global --add safe.directory ${GITHUB_WORKSPACE} 34 | 35 | - name: Install the static SDK 36 | run: | 37 | swift sdk install \ 38 | https://download.swift.org/swift-6.1-release/static-sdk/swift-6.1-RELEASE/swift-6.1-RELEASE_static-linux-0.0.1.artifactbundle.tar.gz \ 39 | --checksum 111c6f7d280a651208b8c74c0521dd99365d785c1976a6e23162f55f65379ac6 40 | 41 | - name: Build the example 42 | run: | 43 | sed -i'.bak' -e "/swift-container-plugin/ s@(url:.*),@(path: \"$PWD\"),@" ${{ matrix.example }}/Package.swift # Use plugin from this checkout 44 | cat ${{ matrix.example }}/Package.swift 45 | swift package \ 46 | --package-path ${{ matrix.example }} \ 47 | -Xswiftc -warnings-as-errors \ 48 | --swift-sdk x86_64-swift-linux-musl \ 49 | --allow-network-connections all \ 50 | build-container-image \ 51 | --repository localhost:5000/example \ 52 | --from scratch 53 | 54 | - name: Run the example 55 | run: | 56 | docker run -d --platform linux/amd64 -p 8080:8080 localhost:5000/example 57 | 58 | - name: Check that the service is running 59 | run: | 60 | # The curious combination of --verbose and --silent causes 61 | # curl to print the request and response (--verbose) but not 62 | # the transmission progress messages (--silent). 63 | # 64 | # --fail-with-body causes curl to exit with a nonzero exit code 65 | # if the HTTP response code is >= 400. Without this flag, curl 66 | # only returns a nonzero exit code if something went wrong while 67 | # connecting to the server - a successful HTTP transaction which 68 | # indicates a server error is still considered to be a successful 69 | # transaction from the client's point of view. 70 | curl --verbose --silent --output /dev/null --fail-with-body localhost:8080 71 | -------------------------------------------------------------------------------- /.github/workflows/integration_tests.yml: -------------------------------------------------------------------------------- 1 | name: Integration tests 2 | 3 | on: 4 | workflow_call: 5 | 6 | jobs: 7 | integration-tests: 8 | name: Integration tests 9 | runs-on: ubuntu-latest 10 | services: 11 | registry: 12 | image: registry:2 13 | ports: 14 | - 5000:5000 15 | container: 16 | image: swift:6.0-noble 17 | steps: 18 | - name: Checkout repository 19 | uses: actions/checkout@v4 20 | with: 21 | persist-credentials: false 22 | 23 | - name: Mark the workspace as safe 24 | # https://github.com/actions/checkout/issues/766 25 | run: git config --global --add safe.directory ${GITHUB_WORKSPACE} 26 | 27 | - name: Install bsdtar 28 | run: | 29 | which bsdtar || (apt-get -q update && apt-get -yq install libarchive-tools) 30 | 31 | - name: Run test job 32 | env: 33 | REGISTRY_HOST: registry 34 | REGISTRY_PORT: 5000 35 | run: | 36 | swift test 37 | 38 | containertool-resources-test: 39 | name: Containertool resources test 40 | runs-on: ubuntu-latest 41 | services: 42 | registry: 43 | image: registry:2 44 | ports: 45 | - 5000:5000 46 | steps: 47 | - name: Checkout repository 48 | uses: actions/checkout@v4 49 | with: 50 | persist-credentials: false 51 | 52 | - name: Mark the workspace as safe 53 | # https://github.com/actions/checkout/issues/766 54 | run: git config --global --add safe.directory ${GITHUB_WORKSPACE} 55 | 56 | - name: Check plugin streaming output is reassembled and printed properly 57 | run: | 58 | scripts/test-containertool-resources.sh 59 | 60 | plugin-streaming-output-test: 61 | name: Plugin streaming output test 62 | runs-on: ubuntu-latest 63 | services: 64 | registry: 65 | image: registry:2 66 | ports: 67 | - 5000:5000 68 | steps: 69 | - name: Checkout repository 70 | uses: actions/checkout@v4 71 | with: 72 | persist-credentials: false 73 | 74 | - name: Mark the workspace as safe 75 | # https://github.com/actions/checkout/issues/766 76 | run: git config --global --add safe.directory ${GITHUB_WORKSPACE} 77 | 78 | - name: Check plugin streaming output is reassembled and printed properly 79 | run: | 80 | scripts/test-plugin-output-streaming.sh 81 | -------------------------------------------------------------------------------- /.github/workflows/interop_tests.yml: -------------------------------------------------------------------------------- 1 | name: Interop tests 2 | 3 | on: 4 | workflow_call: 5 | 6 | jobs: 7 | layering-test: 8 | name: Containertool layering test 9 | runs-on: ubuntu-latest 10 | services: 11 | registry: 12 | image: registry:2 13 | ports: 14 | - 5000:5000 15 | steps: 16 | - name: Checkout repository 17 | uses: actions/checkout@v4 18 | with: 19 | persist-credentials: false 20 | 21 | - name: Mark the workspace as safe 22 | # https://github.com/actions/checkout/issues/766 23 | run: git config --global --add safe.directory ${GITHUB_WORKSPACE} 24 | 25 | # First layer: payload does not have to be an executable, it just has to have known contents 26 | - name: Build first layer 27 | run: | 28 | echo first layer > payload 29 | swift run containertool --repository localhost:5000/layering_test payload --from scratch 30 | docker create --name first --pull always localhost:5000/layering_test 31 | docker cp first:/payload first.payload 32 | grep first first.payload 33 | 34 | # Second layer: payload does not have to be an executable, it just has to have known contents. It should replace the first layer. 35 | - name: Build another layer, which should override 'payload' from the first layer 36 | run: | 37 | echo second layer > payload 38 | swift run containertool --repository localhost:5000/layering_test payload --from localhost:5000/layering_test:latest 39 | docker create --name second --pull always localhost:5000/layering_test 40 | docker cp second:/payload second.payload 41 | grep second second.payload 42 | 43 | elf-detection-test: 44 | name: Containertool ELF detection test 45 | runs-on: ubuntu-latest 46 | services: 47 | registry: 48 | image: registry:2 49 | ports: 50 | - 5000:5000 51 | steps: 52 | - name: Checkout repository 53 | uses: actions/checkout@v4 54 | with: 55 | persist-credentials: false 56 | 57 | - name: Mark the workspace as safe 58 | # https://github.com/actions/checkout/issues/766 59 | run: git config --global --add safe.directory ${GITHUB_WORKSPACE} 60 | 61 | - name: Install the static SDK 62 | run: | 63 | swift sdk install \ 64 | https://download.swift.org/swift-6.1-release/static-sdk/swift-6.1-RELEASE/swift-6.1-RELEASE_static-linux-0.0.1.artifactbundle.tar.gz \ 65 | --checksum 111c6f7d280a651208b8c74c0521dd99365d785c1976a6e23162f55f65379ac6 66 | 67 | # Run the test script 68 | - name: Test ELF detection 69 | run: | 70 | scripts/test-containertool-elf-detection.sh 71 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: main 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | schedule: 8 | - cron: '12 3 * * *' 9 | 10 | jobs: 11 | unit-tests: 12 | name: Unit tests 13 | uses: apple/swift-nio/.github/workflows/unit_tests.yml@main 14 | with: 15 | linux_5_9_enabled: false 16 | linux_5_10_enabled: false 17 | linux_6_0_arguments_override: "--skip SmokeTests --skip TarInteropTests" 18 | linux_6_1_arguments_override: "--skip SmokeTests --skip TarInteropTests" 19 | linux_nightly_6_1_arguments_override: "--skip SmokeTests --skip TarInteropTests" 20 | linux_nightly_main_arguments_override: "--skip SmokeTests --skip TarInteropTests" 21 | 22 | integration-tests: 23 | name: Integration tests 24 | uses: ./.github/workflows/integration_tests.yml 25 | 26 | endtoend-tests: 27 | name: End to end tests 28 | uses: ./.github/workflows/endtoend_tests.yml 29 | -------------------------------------------------------------------------------- /.github/workflows/pull_request.yml: -------------------------------------------------------------------------------- 1 | name: PR 2 | 3 | on: 4 | pull_request: 5 | types: [opened, reopened, synchronize] 6 | 7 | jobs: 8 | soundness: 9 | name: Soundness 10 | uses: swiftlang/github-workflows/.github/workflows/soundness.yml@main 11 | with: 12 | api_breakage_check_container_image: "swift:6.0-noble" 13 | docs_check_container_image: "swift:6.0-noble" 14 | license_header_check_project_name: "SwiftContainerPlugin" 15 | shell_check_container_image: "swift:6.0-noble" 16 | 17 | # Unit tests for functions and modules 18 | unit-tests: 19 | name: Unit tests 20 | uses: apple/swift-nio/.github/workflows/unit_tests.yml@main 21 | with: 22 | linux_5_9_enabled: false 23 | linux_5_10_enabled: false 24 | linux_6_0_arguments_override: "--skip SmokeTests --skip TarInteropTests" 25 | linux_6_1_arguments_override: "--skip SmokeTests --skip TarInteropTests" 26 | linux_nightly_6_1_arguments_override: "--skip SmokeTests --skip TarInteropTests" 27 | linux_nightly_main_arguments_override: "--skip SmokeTests --skip TarInteropTests" 28 | 29 | # Test functions and modules against a separate registry 30 | integration-tests: 31 | name: Integration tests 32 | uses: ./.github/workflows/integration_tests.yml 33 | 34 | # Test that outputs can be handled properly by other systems 35 | interop-tests: 36 | name: Interop tests 37 | uses: ./.github/workflows/interop_tests.yml 38 | 39 | # Full build-package-deploy-run cycles 40 | endtoend-tests: 41 | name: End to end tests 42 | uses: ./.github/workflows/endtoend_tests.yml 43 | 44 | swift-6-language-mode: 45 | name: Swift 6 Language Mode 46 | uses: apple/swift-nio/.github/workflows/swift_6_language_mode.yml@main 47 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | xcuserdata/ 5 | DerivedData/ 6 | .swiftpm/configuration/registries.json 7 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 8 | .netrc 9 | /Package.resolved 10 | -------------------------------------------------------------------------------- /.licenseignore: -------------------------------------------------------------------------------- 1 | **/*.docc/* 2 | **/*.jpg 3 | **/*.md 4 | **/.gitignore 5 | **/Package.resolved 6 | **/Package.swift 7 | **/README.md 8 | .dockerignore 9 | .github/* 10 | .gitignore 11 | .licenseignore 12 | .mailmap 13 | .spi.yml 14 | .swift-format 15 | .swiftformatignore 16 | CODE_OF_CONDUCT.md 17 | CONTRIBUTING.md 18 | CONTRIBUTORS.txt 19 | LICENSE.txt 20 | NOTICE.txt 21 | Package.resolved 22 | Package.swift 23 | README.md 24 | SECURITY.md 25 | Tests/ContainerRegistryTests/Resources/* 26 | Vendor/* 27 | docker-compose.yaml 28 | docker/* 29 | scripts/* 30 | -------------------------------------------------------------------------------- /.mailmap: -------------------------------------------------------------------------------- 1 | Euan Harris 2 | -------------------------------------------------------------------------------- /.spi.yml: -------------------------------------------------------------------------------- 1 | version: 1 2 | builder: 3 | configs: 4 | - documentation_targets: 5 | - swift-container-plugin 6 | -------------------------------------------------------------------------------- /.swift-format: -------------------------------------------------------------------------------- 1 | { 2 | "fileScopedDeclarationPrivacy" : { 3 | "accessLevel" : "private" 4 | }, 5 | "indentation" : { 6 | "spaces" : 4 7 | }, 8 | "indentConditionalCompilationBlocks" : false, 9 | "indentSwitchCaseLabels" : false, 10 | "lineBreakAroundMultilineExpressionChainComponents" : true, 11 | "lineBreakBeforeControlFlowKeywords" : false, 12 | "lineBreakBeforeEachArgument" : true, 13 | "lineBreakBeforeEachGenericRequirement" : true, 14 | "lineLength" : 120, 15 | "maximumBlankLines" : 1, 16 | "prioritizeKeepingFunctionOutputTogether" : false, 17 | "respectsExistingLineBreaks" : true, 18 | "rules" : { 19 | "AllPublicDeclarationsHaveDocumentation" : true, 20 | "AlwaysUseLowerCamelCase" : false, 21 | "AlwaysUseLiteralForEmptyCollectionInit" : true, 22 | "AmbiguousTrailingClosureOverload" : true, 23 | "BeginDocumentationCommentWithOneLineSummary" : false, 24 | "DoNotUseSemicolons" : true, 25 | "DontRepeatTypeInStaticProperties" : false, 26 | "FileScopedDeclarationPrivacy" : true, 27 | "FullyIndirectEnum" : true, 28 | "GroupNumericLiterals" : true, 29 | "IdentifiersMustBeASCII" : true, 30 | "NeverForceUnwrap" : false, 31 | "NeverUseForceTry" : false, 32 | "NeverUseImplicitlyUnwrappedOptionals" : false, 33 | "NoAccessLevelOnExtensionDeclaration" : false, 34 | "NoAssignmentInExpressions" : true, 35 | "NoBlockComments" : true, 36 | "NoCasesWithOnlyFallthrough" : true, 37 | "NoEmptyTrailingClosureParentheses" : true, 38 | "NoLabelsInCasePatterns" : false, 39 | "NoLeadingUnderscores" : false, 40 | "NoParensAroundConditions" : true, 41 | "NoVoidReturnOnFunctionSignature" : true, 42 | "OmitExplicitReturns" : true, 43 | "OneCasePerLine" : true, 44 | "OneVariableDeclarationPerLine" : true, 45 | "OnlyOneTrailingClosureArgument" : true, 46 | "OrderedImports" : false, 47 | "ReplaceForEachWithForLoop" : true, 48 | "ReturnVoidInsteadOfEmptyTuple" : true, 49 | "UseEarlyExits" : false, 50 | "UseLetInEveryBoundCaseVariable" : false, 51 | "UseShorthandTypeNames" : true, 52 | "UseSingleLinePropertyGetter" : false, 53 | "UseSynthesizedInitializer" : true, 54 | "UseTripleSlashForDocumentationComments" : true, 55 | "UseWhereClausesInForLoops" : false, 56 | "ValidateDocumentationComments" : true 57 | }, 58 | "spacesAroundRangeFormationOperators" : false, 59 | "tabWidth" : 8, 60 | "version" : 1 61 | } 62 | -------------------------------------------------------------------------------- /.swiftformatignore: -------------------------------------------------------------------------------- 1 | Vendor/* 2 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | The code of conduct for this project can be found at https://swift.org/code-of-conduct. 4 | 5 | 6 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Legal 2 | 3 | By submitting a pull request, you represent that you have the right to license 4 | your contribution to Apple and the community, and agree by submitting the patch 5 | that your contributions are licensed under the Apache 2.0 license (see 6 | `LICENSE.txt`). 7 | 8 | ## How to submit a bug report 9 | 10 | Please ensure to specify the following: 11 | 12 | * Commit hash 13 | * Contextual information (e.g. what you were trying to achieve with swift-container-plugin) 14 | * Simplest possible steps to reproduce 15 | * More complex the steps are, lower the priority will be. 16 | * A pull request with failing test case is preferred, but it's just fine to paste the test case into the issue description. 17 | * Anything that might be relevant in your opinion, such as: 18 | * Swift version or the output of `swift --version` 19 | * OS version and the output of `uname -a` 20 | * Network configuration 21 | 22 | ### Example 23 | 24 | ``` 25 | Commit hash: b17a8a9f0f814c01a56977680cb68d8a779c951f 26 | 27 | Context: 28 | While testing my application that uses with swift-container-plugin, I noticed that ... 29 | 30 | Steps to reproduce: 31 | 1. ... 32 | 2. ... 33 | 3. ... 34 | 4. ... 35 | 36 | $ swift --version 37 | Swift version 4.0.2 (swift-4.0.2-RELEASE) 38 | Target: x86_64-unknown-linux-gnu 39 | 40 | Operating system: Ubuntu Linux 16.04 64-bit 41 | 42 | $ uname -a 43 | Linux beefy.machine 4.4.0-101-generic #124-Ubuntu SMP Fri Nov 10 18:29:59 UTC 2017 x86_64 x86_64 x86_64 GNU/Linux 44 | 45 | My system has IPv6 disabled. 46 | ``` 47 | 48 | ## Writing a Patch 49 | 50 | A good patch is: 51 | 52 | 1. Concise, and contains as few changes as needed to achieve the end result. 53 | 2. Tested, ensuring that any tests provided failed before the patch and pass after it. 54 | 3. Documented, adding API documentation as needed to cover new functions and properties. 55 | 4. Accompanied by a great commit message, using our commit message template. 56 | 57 | ## How to contribute your work 58 | 59 | Please open a pull request at https://github.com/apple/swift-container-plugin. 60 | Continuous integration (CI) checks for required content (headers), runs a variety of tests, and verifies the documentation builds. All pull requests should pass CI checks before a code review. 61 | -------------------------------------------------------------------------------- /CONTRIBUTORS.txt: -------------------------------------------------------------------------------- 1 | For the purpose of tracking copyright, this is the list of individuals and 2 | organizations who have contributed source code to SwiftContainerPlugin. 3 | 4 | For employees of an organization/company where the copyright of work done 5 | by employees of that company is held by the company itself, only the company 6 | needs to be listed here. 7 | 8 | ## COPYRIGHT HOLDERS 9 | 10 | - Apple Inc. (all contributors with '@apple.com') 11 | 12 | ### Contributors 13 | 14 | - Euan Harris 15 | 16 | **Updating this list** 17 | 18 | Please do not edit this file manually. It is generated using `bash ./scripts/generate-contributors-list.sh`. If a name is misspelled or appearing multiple times: add an entry in `./.mailmap` 19 | -------------------------------------------------------------------------------- /Examples/HelloWorldHummingbird/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | xcuserdata/ 5 | DerivedData/ 6 | .swiftpm/configuration/registries.json 7 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 8 | .netrc 9 | /Package.resolved 10 | -------------------------------------------------------------------------------- /Examples/HelloWorldHummingbird/Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 6.0 2 | 3 | //===----------------------------------------------------------------------===// 4 | // 5 | // This source file is part of the SwiftContainerPlugin open source project 6 | // 7 | // Copyright (c) 2024 Apple Inc. and the SwiftContainerPlugin project authors 8 | // Licensed under Apache License v2.0 9 | // 10 | // See LICENSE.txt for license information 11 | // See CONTRIBUTORS.txt for the list of SwiftContainerPlugin project authors 12 | // 13 | // SPDX-License-Identifier: Apache-2.0 14 | // 15 | //===----------------------------------------------------------------------===// 16 | 17 | import PackageDescription 18 | 19 | let package = Package( 20 | name: "hello-world", 21 | platforms: [.macOS(.v14)], 22 | dependencies: [ 23 | .package(url: "https://github.com/hummingbird-project/hummingbird.git", from: "2.1.0"), 24 | .package(path: "../.."), 25 | .package(url: "https://github.com/apple/swift-argument-parser", from: "1.3.0"), 26 | ], 27 | targets: [ 28 | .executableTarget( 29 | name: "hello-world", 30 | dependencies: [ 31 | .product(name: "Hummingbird", package: "hummingbird"), 32 | .product(name: "ArgumentParser", package: "swift-argument-parser"), 33 | ] 34 | ) 35 | ] 36 | ) 37 | -------------------------------------------------------------------------------- /Examples/HelloWorldHummingbird/Sources/App.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the SwiftContainerPlugin open source project 4 | // 5 | // Copyright (c) 2025 Apple Inc. and the SwiftContainerPlugin project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of SwiftContainerPlugin project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | import ArgumentParser 16 | 17 | @main 18 | struct Hello: AsyncParsableCommand { 19 | @Option(name: .shortAndLong) 20 | var hostname: String = "0.0.0.0" 21 | 22 | @Option(name: .shortAndLong) 23 | var port: Int = 8080 24 | 25 | func run() async throws { 26 | let app = buildApplication( 27 | configuration: .init( 28 | address: .hostname(hostname, port: port), 29 | serverName: "Hummingbird" 30 | ) 31 | ) 32 | try await app.runService() 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Examples/HelloWorldHummingbird/Sources/Application+build.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the SwiftContainerPlugin open source project 4 | // 5 | // Copyright (c) 2025 Apple Inc. and the SwiftContainerPlugin project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of SwiftContainerPlugin project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | import Foundation 16 | import Hummingbird 17 | import Logging 18 | 19 | let myos = ProcessInfo.processInfo.operatingSystemVersionString 20 | 21 | func buildApplication(configuration: ApplicationConfiguration) -> some ApplicationProtocol { 22 | let router = Router() 23 | router.addMiddleware { LogRequestsMiddleware(.info) } 24 | router.get("/") { _, _ in 25 | "Hello World, from Hummingbird on \(myos)\n" 26 | } 27 | 28 | let app = Application( 29 | router: router, 30 | configuration: configuration, 31 | logger: Logger(label: "HelloWorldHummingbird") 32 | ) 33 | 34 | return app 35 | } 36 | -------------------------------------------------------------------------------- /Examples/HelloWorldVapor/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | xcuserdata/ 5 | DerivedData/ 6 | .swiftpm/configuration/registries.json 7 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 8 | .netrc 9 | /Package.resolved 10 | -------------------------------------------------------------------------------- /Examples/HelloWorldVapor/Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 6.0 2 | 3 | //===----------------------------------------------------------------------===// 4 | // 5 | // This source file is part of the SwiftContainerPlugin open source project 6 | // 7 | // Copyright (c) 2024 Apple Inc. and the SwiftContainerPlugin project authors 8 | // Licensed under Apache License v2.0 9 | // 10 | // See LICENSE.txt for license information 11 | // See CONTRIBUTORS.txt for the list of SwiftContainerPlugin project authors 12 | // 13 | // SPDX-License-Identifier: Apache-2.0 14 | // 15 | //===----------------------------------------------------------------------===// 16 | 17 | import PackageDescription 18 | 19 | let package = Package( 20 | name: "hello-world", 21 | platforms: [.macOS(.v13)], 22 | dependencies: [ 23 | .package(url: "https://github.com/vapor/vapor", from: "4.102.0"), 24 | .package(url: "https://github.com/apple/swift-container-plugin", from: "1.0.0"), 25 | ], 26 | targets: [.executableTarget(name: "hello-world", dependencies: [.product(name: "Vapor", package: "vapor")])] 27 | ) 28 | -------------------------------------------------------------------------------- /Examples/HelloWorldVapor/Sources/configure.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the SwiftContainerPlugin open source project 4 | // 5 | // Copyright (c) 2025 Apple Inc. and the SwiftContainerPlugin project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of SwiftContainerPlugin project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | import Vapor 16 | 17 | func configure(_ app: Application) async throws { 18 | try routes(app) 19 | } 20 | -------------------------------------------------------------------------------- /Examples/HelloWorldVapor/Sources/entrypoint.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the SwiftContainerPlugin open source project 4 | // 5 | // Copyright (c) 2025 Apple Inc. and the SwiftContainerPlugin project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of SwiftContainerPlugin project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | import Vapor 16 | 17 | @main 18 | enum Entrypoint { 19 | static func main() async throws { 20 | let env = try Environment.detect() 21 | let app = try await Application.make(env) 22 | app.http.server.configuration.hostname = "0.0.0.0" 23 | 24 | do { 25 | try await configure(app) 26 | try await app.execute() 27 | } catch { 28 | app.logger.report(error: error) 29 | try? await app.asyncShutdown() 30 | throw error 31 | } 32 | try await app.asyncShutdown() 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Examples/HelloWorldVapor/Sources/routes.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the SwiftContainerPlugin open source project 4 | // 5 | // Copyright (c) 2025 Apple Inc. and the SwiftContainerPlugin project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of SwiftContainerPlugin project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | import Foundation 16 | import Vapor 17 | 18 | let myos = ProcessInfo.processInfo.operatingSystemVersionString 19 | 20 | func routes(_ app: Application) throws { 21 | app.get { req async in 22 | "Hello World, from Vapor on \(myos)\n" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Examples/HelloWorldWithResources/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | xcuserdata/ 5 | DerivedData/ 6 | .swiftpm/configuration/registries.json 7 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 8 | .netrc 9 | /Package.resolved 10 | -------------------------------------------------------------------------------- /Examples/HelloWorldWithResources/Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 6.0 2 | 3 | //===----------------------------------------------------------------------===// 4 | // 5 | // This source file is part of the SwiftContainerPlugin open source project 6 | // 7 | // Copyright (c) 2025 Apple Inc. and the SwiftContainerPlugin project authors 8 | // Licensed under Apache License v2.0 9 | // 10 | // See LICENSE.txt for license information 11 | // See CONTRIBUTORS.txt for the list of SwiftContainerPlugin project authors 12 | // 13 | // SPDX-License-Identifier: Apache-2.0 14 | // 15 | //===----------------------------------------------------------------------===// 16 | 17 | import PackageDescription 18 | 19 | let package = Package( 20 | name: "hello-world", 21 | platforms: [.macOS(.v14)], 22 | dependencies: [ 23 | .package(url: "https://github.com/hummingbird-project/hummingbird.git", from: "2.1.0"), 24 | .package(url: "https://github.com/apple/swift-container-plugin", from: "1.0.0"), 25 | .package(url: "https://github.com/apple/swift-argument-parser", from: "1.3.0"), 26 | ], 27 | targets: [ 28 | .executableTarget( 29 | name: "hello-world", 30 | dependencies: [ 31 | .product(name: "Hummingbird", package: "hummingbird"), 32 | .product(name: "ArgumentParser", package: "swift-argument-parser"), 33 | ], 34 | resources: [.process("resources")] 35 | ) 36 | ] 37 | ) 38 | -------------------------------------------------------------------------------- /Examples/HelloWorldWithResources/Sources/App.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the SwiftContainerPlugin open source project 4 | // 5 | // Copyright (c) 2025 Apple Inc. and the SwiftContainerPlugin project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of SwiftContainerPlugin project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | import ArgumentParser 16 | 17 | @main 18 | struct Hello: AsyncParsableCommand { 19 | @Option(name: .shortAndLong) 20 | var hostname: String = "0.0.0.0" 21 | 22 | @Option(name: .shortAndLong) 23 | var port: Int = 8080 24 | 25 | func run() async throws { 26 | let app = buildApplication( 27 | configuration: .init( 28 | address: .hostname(hostname, port: port), 29 | serverName: "Hummingbird" 30 | ) 31 | ) 32 | try await app.runService() 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Examples/HelloWorldWithResources/Sources/Application+build.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the SwiftContainerPlugin open source project 4 | // 5 | // Copyright (c) 2025 Apple Inc. and the SwiftContainerPlugin project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of SwiftContainerPlugin project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | import Foundation 16 | import Hummingbird 17 | import Logging 18 | 19 | let myos = ProcessInfo.processInfo.operatingSystemVersionString 20 | 21 | func buildApplication(configuration: ApplicationConfiguration) -> some ApplicationProtocol { 22 | let router = Router() 23 | router.addMiddleware { LogRequestsMiddleware(.info) } 24 | router.get("/") { _, _ in 25 | let faces = [ 26 | "happy-cat-face", 27 | "slightly-smiling-face", 28 | "smiling-face-with-sunglasses", 29 | ] 30 | 31 | guard let resourceURL = Bundle.module.url(forResource: faces.randomElement(), withExtension: "jpg") else { 32 | throw HTTPError(.internalServerError) 33 | } 34 | 35 | let image = try Data(contentsOf: resourceURL) 36 | 37 | return Response( 38 | status: .ok, 39 | headers: [.contentType: "image/jpg"], 40 | body: .init(byteBuffer: ByteBuffer(bytes: image)) 41 | ) 42 | } 43 | 44 | let app = Application( 45 | router: router, 46 | configuration: configuration, 47 | logger: Logger(label: "hello-with-resources") 48 | ) 49 | 50 | return app 51 | } 52 | -------------------------------------------------------------------------------- /Examples/HelloWorldWithResources/Sources/resources/happy-cat-face.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apple/swift-container-plugin/bf5e256a84ec856e81100d7eab68217d496254c7/Examples/HelloWorldWithResources/Sources/resources/happy-cat-face.jpg -------------------------------------------------------------------------------- /Examples/HelloWorldWithResources/Sources/resources/slightly-smiling-face.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apple/swift-container-plugin/bf5e256a84ec856e81100d7eab68217d496254c7/Examples/HelloWorldWithResources/Sources/resources/slightly-smiling-face.jpg -------------------------------------------------------------------------------- /Examples/HelloWorldWithResources/Sources/resources/smiling-face-with-sunglasses.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apple/swift-container-plugin/bf5e256a84ec856e81100d7eab68217d496254c7/Examples/HelloWorldWithResources/Sources/resources/smiling-face-with-sunglasses.jpg -------------------------------------------------------------------------------- /NOTICE.txt: -------------------------------------------------------------------------------- 1 | 2 | The SwiftContainerPlugin Project 3 | ================================== 4 | 5 | Please visit the SwiftContainerPlugin web site for more information: 6 | 7 | * https://github.com/apple/swift-container-plugin 8 | 9 | Copyright 2024 The SwiftContainerPlugin Project 10 | 11 | The SwiftContainerPlugin Project licenses this file to you under the Apache 12 | License, version 2.0 (the "License"); you may not use this file except in 13 | compliance with the License. You may obtain a copy of the License at: 14 | 15 | https://www.apache.org/licenses/LICENSE-2.0 16 | 17 | Unless required by applicable law or agreed to in writing, software 18 | distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 19 | WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 20 | License for the specific language governing permissions and limitations 21 | under the License. 22 | 23 | Also, please refer to each LICENSE.txt file, which is located in 24 | the 'license' directory of the distribution file, for the license terms of the 25 | components that this product depends on. 26 | 27 | ------------------------------------------------------------------------------- 28 | 29 | This product contains derivations of various scripts and templates from SwiftNIO. 30 | 31 | * LICENSE (Apache License 2.0): 32 | * https://www.apache.org/licenses/LICENSE-2.0 33 | * HOMEPAGE: 34 | * https://github.com/apple/swift-nio 35 | 36 | ------------------------------------------------------------------------------- 37 | 38 | This product contains derivations of templates and proposal workflows from Swift. 39 | 40 | * LICENSE (Apache License 2.0): 41 | * https://swift.org/LICENSE.txt 42 | * HOMEPAGE: 43 | * https://github.com/apple/swift 44 | 45 | ------------------------------------------------------------------------------- 46 | 47 | This product contains derivations of 'AuthorizationProvider' from Swift Package 48 | Manager. 49 | 50 | * LICENSE (Apache License 2.0): 51 | * https://www.apache.org/licenses/LICENSE-2.0 52 | * HOMEPAGE: 53 | * https://github.com/apple/swift-package-manager 54 | 55 | ------------------------------------------------------------------------------- 56 | 57 | This product contains derivations of 'CNIOExtrasZlib' and the 'gzip' 58 | function from SwiftNIO Extras. 59 | 60 | * LICENSE (Apache License 2.0): 61 | * https://www.apache.org/licenses/LICENSE-2.0 62 | * HOMEPAGE: 63 | * https://github.com/swift-server/swift-nio-extras 64 | 65 | ------------------------------------------------------------------------------- 66 | 67 | This product contains samples of how to use Swift Container Plugin with the 68 | following other projects: 69 | 70 | * Distribution Registry 71 | * LICENSE (Apache License 2.0): 72 | * https://www.apache.org/licenses/LICENSE-2.0 73 | * HOMEPAGE: 74 | * https://github.com/distribution/distribution 75 | 76 | * Vapor 77 | * LICENSE (MIT License): 78 | * https://mit-license.org 79 | * HOMEPAGE: 80 | * https://github.com/vapor/vapor 81 | 82 | * Hummingbird 83 | * LICENSE (Apache License 2.0): 84 | * https://www.apache.org/licenses/LICENSE-2.0 85 | * HOMEPAGE: 86 | * https://github.com/hummingbird-project/hummingbird 87 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 6.0 2 | 3 | //===----------------------------------------------------------------------===// 4 | // 5 | // This source file is part of the SwiftContainerPlugin open source project 6 | // 7 | // Copyright (c) 2024 Apple Inc. and the SwiftContainerPlugin project authors 8 | // Licensed under Apache License v2.0 9 | // 10 | // See LICENSE.txt for license information 11 | // See CONTRIBUTORS.txt for the list of SwiftContainerPlugin project authors 12 | // 13 | // SPDX-License-Identifier: Apache-2.0 14 | // 15 | //===----------------------------------------------------------------------===// 16 | 17 | import PackageDescription 18 | 19 | let package = Package( 20 | name: "swift-container-plugin", 21 | platforms: [.macOS(.v13)], 22 | products: [ 23 | .executable(name: "containertool", targets: ["containertool"]), 24 | .plugin(name: "ContainerImageBuilder", targets: ["ContainerImageBuilder"]), 25 | ], 26 | dependencies: [ 27 | .package(url: "https://github.com/apple/swift-crypto.git", "1.0.0"..<"4.0.0"), 28 | .package(url: "https://github.com/apple/swift-argument-parser", from: "1.3.0"), 29 | .package(url: "https://github.com/apple/swift-http-types.git", from: "1.2.0"), 30 | .package(url: "https://github.com/swiftlang/swift-docc-plugin", from: "1.0.0"), 31 | ], 32 | targets: [ 33 | .target( 34 | name: "ContainerRegistry", 35 | dependencies: [ 36 | .target(name: "Basics"), .product(name: "Crypto", package: "swift-crypto"), 37 | .product(name: "HTTPTypes", package: "swift-http-types"), 38 | .product(name: "HTTPTypesFoundation", package: "swift-http-types"), 39 | ] 40 | ), 41 | .executableTarget( 42 | name: "containertool", 43 | dependencies: [ 44 | .target(name: "ContainerRegistry"), 45 | .target(name: "VendorCNIOExtrasZlib"), 46 | .target(name: "Tar"), 47 | .product(name: "ArgumentParser", package: "swift-argument-parser"), 48 | ], 49 | swiftSettings: [.swiftLanguageMode(.v5)] 50 | ), 51 | .target( 52 | // Vendored from https://github.com/apple/swift-nio-extras 53 | name: "VendorCNIOExtrasZlib", 54 | dependencies: [], 55 | path: "Vendor/github.com/apple/swift-nio-extras/Sources/CNIOExtrasZlib", 56 | linkerSettings: [.linkedLibrary("z")] 57 | ), 58 | .target(name: "Tar"), 59 | .target( 60 | // Vendored from https://github.com/apple/swift-package-manager with modifications 61 | name: "Basics", 62 | path: "Vendor/github.com/apple/swift-package-manager/Sources/Basics" 63 | ), 64 | .plugin( 65 | name: "ContainerImageBuilder", 66 | capability: .command( 67 | intent: .custom( 68 | verb: "build-container-image", 69 | description: "Builds a container image for the specified target" 70 | ), 71 | permissions: [ 72 | .allowNetworkConnections( 73 | // scope: .all(ports: [443, 5000, 8080, 70000]), 74 | scope: .all(), 75 | reason: "This command publishes images to container registries over the network" 76 | ) 77 | ] 78 | ), 79 | dependencies: [.target(name: "containertool")] 80 | ), 81 | // Empty target which builds high-level, user-facing documentation about using the plugin from the command-line. 82 | .target( 83 | name: "swift-container-plugin", 84 | exclude: ["README.md"] 85 | ), 86 | .testTarget( 87 | name: "ContainerRegistryTests", 88 | dependencies: [.target(name: "ContainerRegistry")], 89 | resources: [.process("Resources")] 90 | ), 91 | .testTarget(name: "containertoolTests", dependencies: [.target(name: "containertool")]), 92 | .testTarget(name: "TarTests", dependencies: [.target(name: "Tar")]), 93 | ], 94 | swiftLanguageModes: [.v6] 95 | ) 96 | -------------------------------------------------------------------------------- /Plugins/ContainerImageBuilder/Pipe+lines.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the SwiftContainerPlugin open source project 4 | // 5 | // Copyright (c) 2024 Apple Inc. and the SwiftContainerPlugin project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of SwiftContainerPlugin project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | import class Foundation.Pipe 16 | 17 | extension Pipe { 18 | var lines: AsyncThrowingStream { 19 | AsyncThrowingStream { continuation in 20 | self.fileHandleForReading.readabilityHandler = { [unowned self] fileHandle in 21 | // Reading blocks until data is available. We should not see 0 byte reads. 22 | let data = fileHandle.availableData 23 | if data.isEmpty { // EOF 24 | continuation.finish() 25 | 26 | // Clean up the handler to prevent repeated calls and continuation finishes for the same process. 27 | self.fileHandleForReading.readabilityHandler = nil 28 | return 29 | } 30 | 31 | let s = String(data: data, encoding: .utf8)! 32 | continuation.yield(s) 33 | } 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Plugins/ContainerImageBuilder/main.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the SwiftContainerPlugin open source project 4 | // 5 | // Copyright (c) 2024 Apple Inc. and the SwiftContainerPlugin project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of SwiftContainerPlugin project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | import Foundation 16 | import PackagePlugin 17 | 18 | enum PluginError: Error { 19 | case argumentError(String) 20 | case buildError 21 | case productNotExecutable(String) 22 | } 23 | 24 | extension PluginError: CustomStringConvertible { 25 | /// Description of the error 26 | public var description: String { 27 | switch self { 28 | case let .argumentError(s): return s 29 | case .buildError: return "Build failed" 30 | case let .productNotExecutable(productName): 31 | return "\(productName) is not an executable product and cannot be used as a container entrypoint." 32 | } 33 | } 34 | } 35 | 36 | @main struct ContainerImageBuilder: CommandPlugin { 37 | /// Main entry point of the plugin. 38 | /// - Parameters: 39 | /// - context: A `PluginContext` which gives access to Swift Package Manager. 40 | /// - arguments: Additional command line arguments for the plugin. 41 | /// - Throws: If the product cannot be built or if packaging and uploading it fails. 42 | func performCommand(context: PluginContext, arguments: [String]) async throws { 43 | var extractor = ArgumentExtractor(arguments) 44 | if extractor.extractFlag(named: "help") > 0 { 45 | print( 46 | """ 47 | USAGE: build-container-image 48 | 49 | OPTIONS 50 | --product Product to include in the image 51 | 52 | Other arguments are passed to the containertool helper. 53 | """ 54 | ) 55 | return 56 | } 57 | 58 | // The plugin must extract the --product argument, if present, so it can ask Swift Package Manager to rebuild it. 59 | // All other arguments can be passed straight through to the helper tool. 60 | // 61 | // * If --product is specified on the command line, use it. 62 | // * Otherwise, if the package only defines one product, use that. 63 | // * Otherwise there are multiple possible products, so the user must choose which one to use. 64 | 65 | let executableProducts = context.package.products.filter { $0 is ExecutableProduct }.map { $0.name } 66 | 67 | let productName: String 68 | if let productArg = extractor.extractOption(named: "product").first { 69 | guard executableProducts.contains(productArg) else { throw PluginError.productNotExecutable(productArg) } 70 | productName = productArg 71 | } else if executableProducts.count == 1 { 72 | productName = executableProducts[0] 73 | } else { 74 | throw PluginError.argumentError("Please specify which executable product to include in the image") 75 | } 76 | 77 | // Ask the plugin host (SwiftPM or an IDE) to build our product. 78 | Diagnostics.remark("Building product: \(productName)") 79 | let result = try packageManager.build( 80 | .product(productName), 81 | parameters: .init(configuration: .inherit, echoLogs: true) 82 | ) 83 | 84 | // Check the result. Ideally this would report more details. 85 | guard result.succeeded else { throw PluginError.buildError } 86 | 87 | // Get the list of built executables from the build result. 88 | let builtExecutables = result.builtArtifacts.filter { $0.kind == .executable } 89 | 90 | for built in builtExecutables { Diagnostics.remark("Built product: \(built.url.path)") } 91 | 92 | let resources = builtExecutables[0].url 93 | .deletingLastPathComponent() 94 | .appendingPathComponent( 95 | "\(context.package.displayName)_\(productName).resources" 96 | ) 97 | 98 | // Run a command line helper to upload the image 99 | let helperURL = try context.tool(named: "containertool").url 100 | let helperArgs = 101 | (FileManager.default.fileExists(atPath: resources.path) ? ["--resources", resources.path] : []) 102 | + extractor.remainingArguments 103 | + builtExecutables.map { $0.url.path } 104 | let helperEnv = ProcessInfo.processInfo.environment.filter { $0.key.starts(with: "CONTAINERTOOL_") } 105 | 106 | let err = Pipe() 107 | 108 | await withThrowingTaskGroup(of: Void.self) { group in 109 | group.addTask { 110 | enum LoggingState { 111 | // Normal output is logged at 'progress' level. 112 | case progress 113 | 114 | // If an error is detected, all output from that point onwards is logged at 'error' level, which is always printed. 115 | // Errors are reported even without the --verbose flag and cause the build to return a nonzero exit code. 116 | case error 117 | 118 | func log(_ msg: String) { 119 | let trimmed = msg.trimmingCharacters(in: .newlines) 120 | switch self { 121 | case .progress: Diagnostics.progress(trimmed) 122 | case .error: Diagnostics.error(trimmed) 123 | } 124 | } 125 | } 126 | 127 | var buf = "" 128 | var logger = LoggingState.progress 129 | 130 | for try await line in err.lines { 131 | buf.append(line) 132 | 133 | guard let (before, after) = buf.splitOn(first: "\n") else { 134 | continue 135 | } 136 | 137 | var msg = before 138 | buf = String(after) 139 | 140 | let errorLabel = "Error: " // SwiftArgumentParser adds this prefix to all errors which bubble up 141 | if msg.starts(with: errorLabel) { 142 | logger = .error 143 | msg.trimPrefix(errorLabel) 144 | } 145 | 146 | logger.log(String(msg)) 147 | } 148 | 149 | // Print any leftover output in the buffer, in case the child exited without sending a final newline. 150 | if !buf.isEmpty { 151 | logger.log(buf) 152 | } 153 | } 154 | 155 | group.addTask { 156 | try await run(command: helperURL, arguments: helperArgs, environment: helperEnv, errorPipe: err) 157 | } 158 | } 159 | } 160 | } 161 | 162 | extension Collection where Element: Equatable { 163 | func splitOn(first element: Element) -> (before: SubSequence, after: SubSequence)? { 164 | guard let idx = self.firstIndex(of: element) else { 165 | return nil 166 | } 167 | 168 | return (self[.. BearerChallenge { 71 | let nonQuote = try Regex(#"[^"]"#) 72 | let kv = Regex { 73 | Capture { OneOrMore { .word } } 74 | "=" 75 | "\"" 76 | Capture { OneOrMore { nonQuote } } 77 | "\"" 78 | } 79 | let commaKV = Regex { 80 | "," 81 | kv 82 | } 83 | 84 | var res = BearerChallenge() 85 | 86 | var s = Substring(s) 87 | 88 | guard let match = s.prefixMatch(of: kv) else { throw ChallengeParserError.prefixMatchFailed(String(s)) } 89 | 90 | switch match.1 { 91 | case "realm": res.realm = String(match.2) 92 | case "service": res.service = String(match.2) 93 | case "scope": res.scope.append(String(match.2)) 94 | default: res.other.append((String(match.1), String(match.2))) 95 | } 96 | s.trimPrefix(match.0) 97 | 98 | while let match = s.prefixMatch(of: commaKV) { 99 | switch match.1 { 100 | case "realm": res.realm = String(match.2) 101 | case "service": res.service = String(match.2) 102 | case "scope": res.scope.append(String(match.2)) 103 | default: res.other.append((String(match.1), String(match.2))) 104 | } 105 | s.trimPrefix(match.0) 106 | } 107 | 108 | if s != "" { throw ChallengeParserError.leftoverCharacters(String(s)) } 109 | 110 | return res 111 | } 112 | 113 | public enum AuthChallenge: Equatable { 114 | case none 115 | case basic(String) 116 | case bearer(String) 117 | 118 | init(challenge: String) { 119 | if challenge.lowercased().starts(with: "basic") { 120 | self = .basic(challenge) 121 | } else if challenge.lowercased().starts(with: "bearer") { 122 | self = .bearer(challenge) 123 | } else { 124 | self = .none 125 | } 126 | } 127 | } 128 | 129 | /// AuthHandler manages provides credentials for HTTP requests 130 | public struct AuthHandler { 131 | var username: String? 132 | var password: String? 133 | 134 | var auth: AuthorizationProvider? = nil 135 | /// Create an AuthHandler 136 | /// - Parameters: 137 | /// - username: Default username, used if no other suitable credentials are available. 138 | /// - password: Default password, used if no other suitable credentials are available. 139 | /// - auth: AuthorizationProvider capable of querying credential stores such as netrc files. 140 | public init(username: String? = nil, password: String? = nil, auth: AuthorizationProvider? = nil) { 141 | self.username = username 142 | self.password = password 143 | self.auth = auth 144 | } 145 | 146 | /// Get locally-configured credentials, such as netrc or username/password, for a host 147 | func localCredentials(forURL url: URL) -> String? { 148 | if let netrcEntry = auth?.httpAuthorizationHeader(for: url) { return netrcEntry } 149 | 150 | if let username, let password { 151 | let authorization = Data("\(username):\(password)".utf8).base64EncodedString() 152 | return "Basic \(authorization)" 153 | } 154 | 155 | // No suitable authentication methods available 156 | return nil 157 | } 158 | 159 | public func auth( 160 | registry: URL, 161 | repository: ImageReference.Repository, 162 | actions: [String], 163 | withScheme scheme: AuthChallenge, 164 | usingClient client: HTTPClient 165 | ) async throws -> String? { 166 | switch scheme { 167 | case .none: return nil 168 | case .basic: return localCredentials(forURL: registry) 169 | 170 | case .bearer(let challenge): 171 | // Preemptively offer suitable basic auth credentials to the token server. 172 | // Instead of challenging, public token servers often return anonymous tokens when no credentials are offered. 173 | // These tokens allow pull access to public repositories, but attempts to push will fail with 'unauthorized'. 174 | // There is no obvious prompt for the client to retry with authentication. 175 | var parsedChallenge = try parseChallenge( 176 | challenge.dropFirst("bearer".count).trimmingCharacters(in: .whitespacesAndNewlines) 177 | ) 178 | parsedChallenge.scope = ["repository:\(repository):\(actions.joined(separator: ","))"] 179 | guard let challengeURL = parsedChallenge.url else { return nil } 180 | var tokenRequest = HTTPRequest(url: challengeURL) 181 | if let credentials = localCredentials(forURL: challengeURL) { 182 | tokenRequest.headerFields[.authorization] = credentials 183 | } 184 | 185 | let (data, _) = try await client.executeRequestThrowing(tokenRequest, expectingStatus: .ok) 186 | let tokenResponse = try JSONDecoder().decode(BearerTokenResponse.self, from: data) 187 | return "Bearer \(tokenResponse.token)" 188 | } 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /Sources/ContainerRegistry/Blobs.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the SwiftContainerPlugin open source project 4 | // 5 | // Copyright (c) 2024 Apple Inc. and the SwiftContainerPlugin project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of SwiftContainerPlugin project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | import Foundation 16 | import HTTPTypes 17 | import struct Crypto.SHA256 18 | 19 | /// Calculates the digest of a blob of data. 20 | /// - Parameter data: Blob of data to digest. 21 | /// - Returns: The blob's digest, in the format expected by the distribution protocol. 22 | public func digest(of data: D) -> String { 23 | // SHA256 is required; some registries might also support SHA512 24 | let hash = SHA256.hash(data: data) 25 | let digest = hash.compactMap { String(format: "%02x", $0) }.joined() 26 | return "sha256:" + digest 27 | } 28 | 29 | extension RegistryClient { 30 | // Internal helper method to initiate a blob upload in 'two shot' mode 31 | func startBlobUploadSession(repository: ImageReference.Repository) async throws -> URL { 32 | // Upload in "two shot" mode. 33 | // See https://github.com/opencontainers/distribution-spec/blob/main/spec.md#post-then-put 34 | // - POST to obtain a session ID. 35 | // - Do not include the digest. 36 | // Response will include a 'Location' header telling us where to PUT the blob data. 37 | let httpResponse = try await executeRequestThrowing( 38 | .post(repository, path: "blobs/uploads/"), 39 | expectingStatus: .accepted, // expected response code for a "two-shot" upload 40 | decodingErrors: [.notFound] 41 | ) 42 | 43 | guard let location = httpResponse.response.headerFields[.location] else { 44 | throw HTTPClientError.missingResponseHeader("Location") 45 | } 46 | 47 | guard let locationURL = URL(string: location) else { 48 | throw RegistryClientError.invalidUploadLocation("\(location)") 49 | } 50 | 51 | // The location may be either an absolute URL or a relative URL 52 | // If it is relative we need to make it absolute 53 | guard locationURL.host != nil else { 54 | guard let absoluteURL = URL(string: location, relativeTo: registryURL) else { 55 | throw RegistryClientError.invalidUploadLocation("\(location)") 56 | } 57 | return absoluteURL 58 | } 59 | 60 | return locationURL 61 | } 62 | } 63 | 64 | // The spec says that Docker- prefix headers are no longer to be used, but also specifies that the registry digest is returned in this header. 65 | extension HTTPField.Name { static let dockerContentDigest = Self("Docker-Content-Digest")! } 66 | 67 | public extension RegistryClient { 68 | func blobExists(repository: ImageReference.Repository, digest: String) async throws -> Bool { 69 | precondition(digest.count > 0) 70 | 71 | do { 72 | let _ = try await executeRequestThrowing( 73 | .head(repository, path: "blobs/\(digest)"), 74 | decodingErrors: [.notFound] 75 | ) 76 | return true 77 | } catch HTTPClientError.unexpectedStatusCode(status: .notFound, _, _) { return false } 78 | } 79 | 80 | /// Fetches an unstructured blob of data from the registry. 81 | /// 82 | /// - Parameters: 83 | /// - repository: Name of the repository containing the blob. 84 | /// - digest: Digest of the blob. 85 | /// - Returns: The downloaded data. 86 | /// - Throws: If the blob download fails. 87 | func getBlob(repository: ImageReference.Repository, digest: String) async throws -> Data { 88 | precondition(digest.count > 0, "digest must not be an empty string") 89 | 90 | return try await executeRequestThrowing( 91 | .get(repository, path: "blobs/\(digest)", accepting: ["application/octet-stream"]), 92 | decodingErrors: [.notFound] 93 | ) 94 | .data 95 | } 96 | 97 | /// Fetches a blob and tries to decode it as a JSON object. 98 | /// 99 | /// - Parameters: 100 | /// - repository: Name of the repository containing the blob. 101 | /// - digest: Digest of the blob. 102 | /// - Returns: The decoded object. 103 | /// - Throws: If the blob download fails or the blob cannot be decoded. 104 | /// 105 | /// Some JSON objects, such as ImageConfiguration, are stored 106 | /// in the registry as plain blobs with MIME type "application/octet-stream". 107 | /// This function attempts to decode the received data without reference 108 | /// to the MIME type. 109 | func getBlob(repository: ImageReference.Repository, digest: String) async throws -> Response { 110 | precondition(digest.count > 0, "digest must not be an empty string") 111 | 112 | return try await executeRequestThrowing( 113 | .get(repository, path: "blobs/\(digest)", accepting: ["application/octet-stream"]), 114 | decodingErrors: [.notFound] 115 | ) 116 | .data 117 | } 118 | 119 | /// Uploads a blob to the registry. 120 | /// 121 | /// This function uploads a blob of unstructured data to the registry. 122 | /// - Parameters: 123 | /// - repository: Name of the destination repository. 124 | /// - mediaType: mediaType field for returned ContentDescriptor. 125 | /// On the wire, all blob uploads are `application/octet-stream'. 126 | /// - data: Object to be uploaded. 127 | /// - Returns: An ContentDescriptor object representing the 128 | /// uploaded blob. 129 | /// - Throws: If the blob cannot be encoded or the upload fails. 130 | func putBlob(repository: ImageReference.Repository, mediaType: String = "application/octet-stream", data: Data) 131 | async throws 132 | -> ContentDescriptor 133 | { 134 | // Ask the server to open a session and tell us where to upload our data 135 | let location = try await startBlobUploadSession(repository: repository) 136 | 137 | // Append the digest to the upload location, as the specification requires. 138 | // The server's URL is arbitrary and might already contain query items which we must not overwrite. 139 | // The URL could even point to a different host. 140 | let digest = digest(of: data) 141 | let uploadURL = location.appending(queryItems: [.init(name: "digest", value: "\(digest.utf8)")]) 142 | 143 | let httpResponse = try await executeRequestThrowing( 144 | // All blob uploads have Content-Type: application/octet-stream on the wire, even if mediatype is different 145 | .put(repository, url: uploadURL, contentType: "application/octet-stream"), 146 | uploading: data, 147 | expectingStatus: .created, 148 | decodingErrors: [.badRequest, .notFound] 149 | ) 150 | 151 | // The registry could compute a different digest and we should use its value 152 | // as the canonical digest for linking blobs. If the registry sends a digest we 153 | // should check that it matches our locally-calculated digest. 154 | if let serverDigest = httpResponse.response.headerFields[.dockerContentDigest] { 155 | assert(digest == serverDigest) 156 | } 157 | return .init(mediaType: mediaType, digest: digest, size: Int64(data.count)) 158 | } 159 | 160 | /// Uploads a blob to the registry. 161 | /// 162 | /// This function converts an encodable blob to an `application/octet-stream', 163 | /// calculates its digest and uploads it to the registry. 164 | /// - Parameters: 165 | /// - repository: Name of the destination repository. 166 | /// - mediaType: mediaType field for returned ContentDescriptor. 167 | /// On the wire, all blob uploads are `application/octet-stream'. 168 | /// - data: Object to be uploaded. 169 | /// - Returns: An ContentDescriptor object representing the 170 | /// uploaded blob. 171 | /// - Throws: If the blob cannot be encoded or the upload fails. 172 | /// 173 | /// Some JSON objects, such as ImageConfiguration, are stored 174 | /// in the registry as plain blobs with MIME type "application/octet-stream". 175 | /// This function encodes the data parameter and uploads it as a generic blob. 176 | func putBlob( 177 | repository: ImageReference.Repository, 178 | mediaType: String = "application/octet-stream", 179 | data: Body 180 | ) 181 | async throws -> ContentDescriptor 182 | { 183 | let encoded = try encoder.encode(data) 184 | return try await putBlob(repository: repository, mediaType: mediaType, data: encoded) 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /Sources/ContainerRegistry/CheckAPI.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the SwiftContainerPlugin open source project 4 | // 5 | // Copyright (c) 2024 Apple Inc. and the SwiftContainerPlugin project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of SwiftContainerPlugin project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | import Foundation 16 | 17 | public extension RegistryClient { 18 | /// Checks whether the registry supports v2 of the distribution specification. 19 | /// - Returns: an `true` if the registry supports the distribution specification. 20 | /// - Throws: if the registry does not support the distribution specification. 21 | static func checkAPI(client: HTTPClient, registryURL: URL) async throws -> AuthChallenge { 22 | // See https://github.com/opencontainers/distribution-spec/blob/main/spec.md#determining-support 23 | 24 | // The registry indicates that it supports the v2 protocol by returning a 200 OK response. 25 | // Many registries also set `Content-Type: application/json` and return empty JSON objects, 26 | // but this is not required and some do not. 27 | // The registry may require authentication on this endpoint. 28 | 29 | do { 30 | // Using the bare HTTP client because this is the only endpoint which does not include a repository path 31 | // and to avoid RegistryClient's auth handling 32 | let _ = try await client.executeRequestThrowing( 33 | .get(registryURL.distributionEndpoint, withAuthorization: nil), 34 | expectingStatus: .ok 35 | ) 36 | return .none 37 | 38 | } catch HTTPClientError.authenticationChallenge(let challenge, _, _) { return .init(challenge: challenge) } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Sources/ContainerRegistry/HTTPClient.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the SwiftContainerPlugin open source project 4 | // 5 | // Copyright (c) 2024 Apple Inc. and the SwiftContainerPlugin project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of SwiftContainerPlugin project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | import Foundation 16 | #if canImport(FoundationNetworking) 17 | import FoundationNetworking 18 | #endif 19 | import HTTPTypes 20 | import HTTPTypesFoundation 21 | 22 | // HEAD does not include a response body so if an error is thrown, data will be nil 23 | public enum HTTPClientError: Error { 24 | case unexpectedStatusCode(status: HTTPResponse.Status, response: HTTPResponse, data: Data?) 25 | case unexpectedContentType(String) 26 | case missingContentType 27 | case missingResponseHeader(String) 28 | case authenticationChallenge(challenge: String, request: HTTPRequest, response: HTTPResponse) 29 | case unauthorized(request: HTTPRequest, response: HTTPResponse) 30 | } 31 | 32 | /// HTTPClient is an abstract HTTP client interface capable of uploads and downloads. 33 | public protocol HTTPClient { 34 | /// Execute an HTTP request with no request body. 35 | /// - Parameters: 36 | /// - request: The HTTP request to execute. 37 | /// - expectingStatus: The HTTP status code expected if the request is successful. 38 | /// - Returns: An asynchronously-delivered tuple that contains the raw response body as a Data instance, and a HTTPResponse. 39 | /// - Throws: If the server response is unexpected or indicates that an error occurred. 40 | func executeRequestThrowing(_ request: HTTPRequest, expectingStatus: HTTPResponse.Status) async throws -> ( 41 | Data, HTTPResponse 42 | ) 43 | 44 | /// Execute an HTTP request uploading a request body. 45 | /// - Parameters: 46 | /// - request: The HTTP request to execute. 47 | /// - uploading: The request body to upload. 48 | /// - expectingStatus: The HTTP status code expected if the request is successful. 49 | /// - Returns: An asynchronously-delivered tuple that contains the raw response body as a Data instance, and a HTTPResponse. 50 | /// - Throws: If the server response is unexpected or indicates that an error occurred. 51 | func executeRequestThrowing(_ request: HTTPRequest, uploading: Data, expectingStatus: HTTPResponse.Status) 52 | async throws -> (Data, HTTPResponse) 53 | } 54 | 55 | extension URLSession: HTTPClient { 56 | /// Check that a registry response has the correct status code and does not report an error. 57 | /// - Parameters: 58 | /// - request: The request made to the registry. 59 | /// - response: The response from the registry. 60 | /// - responseData: The raw response body data returned by the registry. 61 | /// - successfulStatus: The successful HTTP response expected from this request. 62 | /// - Returns: An HTTPResponse representing the response, if the response was valid. 63 | /// - Throws: If the server response is unexpected or indicates that an error occurred. 64 | func validateAPIResponseThrowing( 65 | request: HTTPRequest, 66 | response: HTTPResponse, 67 | responseData: Data, 68 | expectingStatus successfulStatus: HTTPResponse.Status 69 | ) throws -> HTTPResponse { 70 | // Convert errors into exceptions 71 | guard response.status == successfulStatus else { 72 | // If the response includes an authentication challenge the client can try again, 73 | // presenting the challenge response. 74 | if response.status == .unauthorized { 75 | if let authChallenge = response.headerFields[.wwwAuthenticate] { 76 | throw HTTPClientError.authenticationChallenge( 77 | challenge: authChallenge.trimmingCharacters(in: .whitespacesAndNewlines), 78 | request: request, 79 | response: response 80 | ) 81 | } 82 | } 83 | 84 | // A HEAD request has no response body and cannot be decoded 85 | if request.method == .head { 86 | throw HTTPClientError.unexpectedStatusCode(status: response.status, response: response, data: nil) 87 | } 88 | throw HTTPClientError.unexpectedStatusCode(status: response.status, response: response, data: responseData) 89 | } 90 | 91 | return response 92 | } 93 | 94 | /// Execute an HTTP request with no request body. 95 | /// - Parameters: 96 | /// - request: The HTTP request to execute. 97 | /// - success: The HTTP status code expected if the request is successful. 98 | /// - Returns: An asynchronously-delivered tuple that contains the raw response body as a Data instance, and a HTTPResponse. 99 | /// - Throws: If the server response is unexpected or indicates that an error occurred. 100 | public func executeRequestThrowing(_ request: HTTPRequest, expectingStatus success: HTTPResponse.Status) 101 | async throws -> (Data, HTTPResponse) 102 | { 103 | let (responseData, urlResponse) = try await data(for: request) 104 | let httpResponse = try validateAPIResponseThrowing( 105 | request: request, 106 | response: urlResponse, 107 | responseData: responseData, 108 | expectingStatus: success 109 | ) 110 | return (responseData, httpResponse) 111 | } 112 | 113 | /// Execute an HTTP request uploading a request body. 114 | /// - Parameters: 115 | /// - request: The HTTP request to execute. 116 | /// - payload: The request body to upload. 117 | /// - success: The HTTP status code expected if the request is successful. 118 | /// - Returns: An asynchronously-delivered tuple that contains the raw response body as a Data instance, and a HTTPResponse. 119 | /// - Throws: If the server response is unexpected or indicates that an error occurred. 120 | public func executeRequestThrowing( 121 | _ request: HTTPRequest, 122 | uploading payload: Data, 123 | expectingStatus success: HTTPResponse.Status 124 | ) async throws -> (Data, HTTPResponse) { 125 | let (responseData, urlResponse) = try await upload(for: request, from: payload) 126 | let httpResponse = try validateAPIResponseThrowing( 127 | request: request, 128 | response: urlResponse, 129 | responseData: responseData, 130 | expectingStatus: success 131 | ) 132 | return (responseData, httpResponse) 133 | } 134 | } 135 | 136 | extension HTTPRequest { 137 | /// Constructs a HTTPRequest pre-configured with method, url and content types. 138 | /// - Parameters: 139 | /// - method: HTTP method to use: "GET", "PUT" etc 140 | /// - url: The URL on which to operate. 141 | /// - accepting: A list of acceptable content-types. 142 | /// - contentType: The content-type of the request's body data, if any. 143 | /// - authorization: Authorization credentials for this request. 144 | init( 145 | method: HTTPRequest.Method, 146 | url: URL, 147 | accepting: [String] = [], 148 | contentType: String? = nil, 149 | withAuthorization authorization: String? = nil 150 | ) { 151 | self.init(url: url) 152 | self.method = method 153 | if let contentType { headerFields[.contentType] = contentType } 154 | if accepting.count > 0 { headerFields[values: .accept] = accepting } 155 | 156 | // The URLSession documentation warns not to do this: 157 | // https://developer.apple.com/documentation/foundation/urlsessionconfiguration/1411532-httpadditionalheaders#discussion 158 | // However this is the best option when URLSession does not support the server's authentication scheme: 159 | // https://developer.apple.com/forums/thread/89811 160 | if let authorization { headerFields[.authorization] = authorization } 161 | } 162 | 163 | static func get( 164 | _ url: URL, 165 | accepting: [String] = [], 166 | contentType: String? = nil, 167 | withAuthorization authorization: String? = nil 168 | ) -> HTTPRequest { 169 | .init(method: .get, url: url, accepting: accepting, contentType: contentType, withAuthorization: authorization) 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /Sources/ContainerRegistry/ImageManifest+Digest.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the SwiftContainerPlugin open source project 4 | // 5 | // Copyright (c) 2024 Apple Inc. and the SwiftContainerPlugin project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of SwiftContainerPlugin project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | import Foundation 16 | import struct Crypto.SHA256 17 | 18 | public extension ImageManifest { 19 | var digest: String { 20 | let encoder = JSONEncoder() 21 | encoder.outputFormatting = [.sortedKeys, .prettyPrinted] 22 | encoder.dateEncodingStrategy = .iso8601 23 | let encoded = try! encoder.encode(self) 24 | return ContainerRegistry.digest(of: encoded) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Sources/ContainerRegistry/ImageReference.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the SwiftContainerPlugin open source project 4 | // 5 | // Copyright (c) 2024 Apple Inc. and the SwiftContainerPlugin project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of SwiftContainerPlugin project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | import RegexBuilder 16 | 17 | // https://github.com/distribution/distribution/blob/v2.7.1/reference/reference.go 18 | // Split the image reference into a registry and a name part. 19 | func splitReference(_ reference: String) throws -> (String?, String) { 20 | let splits = reference.split(separator: "/", maxSplits: 1, omittingEmptySubsequences: false) 21 | if splits.count == 0 { throw ImageReference.ValidationError.unexpected("unexpected error") } 22 | 23 | if splits.count == 1 { return (nil, reference) } 24 | 25 | // assert splits == 2 26 | // Hostname heuristic: contains a '.' or a ':', or is localhost 27 | if splits[0] != "localhost", !splits[0].contains("."), !splits[0].contains(":") { return (nil, reference) } 28 | 29 | return (String(splits[0]), String(splits[1])) 30 | } 31 | 32 | // Split the name into repository and tag parts 33 | // distribution/distribution defines regular expressions which validate names but these seem to be very strict 34 | // and reject names which clients accept 35 | func splitName(_ name: String) throws -> (String, String) { 36 | let digestSplit = name.split(separator: "@", maxSplits: 1, omittingEmptySubsequences: false) 37 | if digestSplit.count == 2 { return (String(digestSplit[0]), String(digestSplit[1])) } 38 | 39 | let tagSplit = name.split(separator: ":", maxSplits: 1, omittingEmptySubsequences: false) 40 | if tagSplit.count == 0 { throw ImageReference.ValidationError.unexpected("unexpected error") } 41 | 42 | if tagSplit.count == 1 { return (name, "latest") } 43 | 44 | // assert splits == 2 45 | return (String(tagSplit[0]), String(tagSplit[1])) 46 | } 47 | 48 | /// ImageReference points to an image stored on a container registry 49 | public struct ImageReference: Sendable, Equatable, CustomStringConvertible, CustomDebugStringConvertible { 50 | /// The registry which contains this image 51 | public var registry: String 52 | /// The repository which contains this image 53 | public var repository: Repository 54 | /// The tag identifying the image. 55 | public var reference: String 56 | 57 | public enum ValidationError: Error { 58 | case unexpected(String) 59 | } 60 | 61 | /// Creates an ImageReference from an image reference string. 62 | /// - Parameters: 63 | /// - reference: The reference to parse. 64 | /// - defaultRegistry: The default registry to use if the reference does not include a registry. 65 | /// - Throws: If `reference` cannot be parsed as an image reference. 66 | public init(fromString reference: String, defaultRegistry: String = "localhost:5000") throws { 67 | let (registry, remainder) = try splitReference(reference) 68 | let (repository, reference) = try splitName(remainder) 69 | self.registry = registry ?? defaultRegistry 70 | if self.registry == "docker.io" { 71 | self.registry = "index.docker.io" // Special case for docker client, there is no network-level redirect 72 | } 73 | // As a special case, official images can be referred to by a single name, such as `swift` or `swift:slim`. 74 | // moby/moby assumes that these names refer to images in `library`: `library/swift` or `library/swift:slim`. 75 | // This special case only applies when using Docker Hub, so `example.com/swift` is not expanded `example.com/library/swift` 76 | if self.registry == "index.docker.io" && !repository.contains("/") { 77 | self.repository = try Repository("library/\(repository)") 78 | } else { 79 | self.repository = try Repository(repository) 80 | } 81 | self.reference = reference 82 | } 83 | 84 | /// Creates an ImageReference from separate registry, repository and reference strings. 85 | /// Used only in tests. 86 | /// - Parameters: 87 | /// - registry: The registry which stores the image data. 88 | /// - repository: The repository within the registry which holds the image. 89 | /// - reference: The tag identifying the image. 90 | init(registry: String, repository: Repository, reference: String) { 91 | self.registry = registry 92 | self.repository = repository 93 | self.reference = reference 94 | } 95 | 96 | /// Printable description of an ImageReference in a form which can be understood by a runtime 97 | public var description: String { 98 | if reference.starts(with: "sha256") { 99 | return "\(registry)/\(repository)@\(reference)" 100 | } else { 101 | return "\(registry)/\(repository):\(reference)" 102 | } 103 | } 104 | 105 | /// Printable description of an ImageReference in a form suitable for debugging. 106 | public var debugDescription: String { 107 | "ImageReference(registry: \(registry), repository: \(repository), reference: \(reference))" 108 | } 109 | } 110 | 111 | extension ImageReference { 112 | /// Repository refers a repository (image namespace) on a container registry 113 | public struct Repository: Sendable, Equatable, CustomStringConvertible, CustomDebugStringConvertible { 114 | var value: String 115 | 116 | public enum ValidationError: Error, Equatable { 117 | case emptyString 118 | case containsUppercaseLetters(String) 119 | case invalidReferenceFormat(String) 120 | } 121 | 122 | public init(_ rawValue: String) throws { 123 | // Reference handling in github.com/distribution reports empty and uppercase as specific errors. 124 | // All other errors caused are reported as generic format errors. 125 | guard rawValue.count > 0 else { 126 | throw ValidationError.emptyString 127 | } 128 | 129 | if (rawValue.contains { $0.isUppercase }) { 130 | throw ValidationError.containsUppercaseLetters(rawValue) 131 | } 132 | 133 | // https://github.com/opencontainers/distribution-spec/blob/main/spec.md#pulling-manifests 134 | let regex = /[a-z0-9]+((\.|_|__|-+)[a-z0-9]+)*(\/[a-z0-9]+((\.|_|__|-+)[a-z0-9]+)*)*/ 135 | if try regex.wholeMatch(in: rawValue) == nil { 136 | throw ValidationError.invalidReferenceFormat(rawValue) 137 | } 138 | 139 | value = rawValue 140 | } 141 | 142 | public var description: String { 143 | value 144 | } 145 | 146 | /// Printable description of an ImageReference in a form suitable for debugging. 147 | public var debugDescription: String { 148 | "Repository(\(value))" 149 | } 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /Sources/ContainerRegistry/Manifests.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the SwiftContainerPlugin open source project 4 | // 5 | // Copyright (c) 2024 Apple Inc. and the SwiftContainerPlugin project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of SwiftContainerPlugin project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | public extension RegistryClient { 16 | func putManifest(repository: ImageReference.Repository, reference: String, manifest: ImageManifest) async throws 17 | -> String 18 | { 19 | // See https://github.com/opencontainers/distribution-spec/blob/main/spec.md#pushing-manifests 20 | precondition("\(reference)".count > 0, "reference must not be an empty string") 21 | 22 | let httpResponse = try await executeRequestThrowing( 23 | // All blob uploads have Content-Type: application/octet-stream on the wire, even if mediatype is different 24 | .put( 25 | repository, 26 | path: "manifests/\(reference)", 27 | contentType: manifest.mediaType ?? "application/vnd.oci.image.manifest.v1+json" 28 | ), 29 | uploading: manifest, 30 | expectingStatus: .created, 31 | decodingErrors: [.notFound] 32 | ) 33 | 34 | // The distribution spec says the response MUST contain a Location header 35 | // providing a URL from which the saved manifest can be downloaded. 36 | // However some registries return URLs which cannot be fetched, and 37 | // ECR does not set this header at all. 38 | // If the header is not present, create a suitable value. 39 | // https://github.com/opencontainers/distribution-spec/blob/main/spec.md#pulling-manifests 40 | return httpResponse.response.headerFields[.location] 41 | ?? registryURL.distributionEndpoint(forRepository: repository, andEndpoint: "manifests/\(manifest.digest)") 42 | .absoluteString 43 | } 44 | 45 | func getManifest(repository: ImageReference.Repository, reference: String) async throws -> ImageManifest { 46 | // See https://github.com/opencontainers/distribution-spec/blob/main/spec.md#pulling-manifests 47 | precondition(reference.count > 0, "reference must not be an empty string") 48 | 49 | return try await executeRequestThrowing( 50 | .get( 51 | repository, 52 | path: "manifests/\(reference)", 53 | accepting: [ 54 | "application/vnd.oci.image.manifest.v1+json", 55 | "application/vnd.docker.distribution.manifest.v2+json", 56 | ] 57 | ), 58 | decodingErrors: [.notFound] 59 | ) 60 | .data 61 | } 62 | 63 | func getIndex(repository: ImageReference.Repository, reference: String) async throws -> ImageIndex { 64 | precondition(reference.count > 0, "reference must not be an empty string") 65 | 66 | return try await executeRequestThrowing( 67 | .get( 68 | repository, 69 | path: "manifests/\(reference)", 70 | accepting: [ 71 | "application/vnd.oci.image.index.v1+json", 72 | "application/vnd.docker.distribution.manifest.list.v2+json", 73 | ] 74 | ), 75 | decodingErrors: [.notFound] 76 | ) 77 | .data 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /Sources/ContainerRegistry/RegistryClient+ImageConfiguration.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the SwiftContainerPlugin open source project 4 | // 5 | // Copyright (c) 2024 Apple Inc. and the SwiftContainerPlugin project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of SwiftContainerPlugin project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | extension RegistryClient { 16 | /// Get an image configuration record from the registry. 17 | /// - Parameters: 18 | /// - image: Reference to the image containing the record. 19 | /// - digest: Digest of the record. 20 | /// - Returns: The image confguration record stored in `repository` with digest `digest`. 21 | /// - Throws: If the blob cannot be decoded as an `ImageConfiguration`. 22 | /// 23 | /// Image configuration records are stored as blobs in the registry. This function retrieves the requested blob and tries to decode it as a configuration record. 24 | public func getImageConfiguration(forImage image: ImageReference, digest: String) async throws -> ImageConfiguration 25 | { 26 | try await getBlob(repository: image.repository, digest: digest) 27 | } 28 | 29 | /// Upload an image configuration record to the registry. 30 | /// - Parameters: 31 | /// - image: Reference to the image associated with the record. 32 | /// - configuration: An image configuration record 33 | /// - Returns: An `ContentDescriptor` referring to the blob stored in the registry. 34 | /// - Throws: If the blob upload fails. 35 | /// 36 | /// Image configuration records are stored as blobs in the registry. This function encodes the provided configuration record and stores it as a blob in the registry. 37 | public func putImageConfiguration(forImage image: ImageReference, configuration: ImageConfiguration) async throws 38 | -> ContentDescriptor 39 | { 40 | try await putBlob( 41 | repository: image.repository, 42 | mediaType: "application/vnd.oci.image.config.v1+json", 43 | data: configuration 44 | ) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Sources/ContainerRegistry/Tags.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the SwiftContainerPlugin open source project 4 | // 5 | // Copyright (c) 2024 Apple Inc. and the SwiftContainerPlugin project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of SwiftContainerPlugin project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | public extension RegistryClient { 16 | func getTags(repository: ImageReference.Repository) async throws -> Tags { 17 | // See https://github.com/opencontainers/distribution-spec/blob/main/spec.md#listing-tags 18 | try await executeRequestThrowing(.get(repository, path: "tags/list"), decodingErrors: [.notFound]).data 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Sources/containertool/ELFDetect.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the SwiftContainerPlugin open source project 4 | // 5 | // Copyright (c) 2025 Apple Inc. and the SwiftContainerPlugin project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of SwiftContainerPlugin project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | import class Foundation.FileHandle 16 | import struct Foundation.URL 17 | 18 | struct ArrayField where T.Element == UInt8 { 19 | var start: Int 20 | var count: Int 21 | } 22 | 23 | struct IntField { 24 | var start: Int 25 | } 26 | 27 | extension Array where Element == UInt8 { 28 | subscript(idx: ArrayField<[UInt8]>) -> [UInt8] { 29 | [UInt8](self[idx.start..) -> UInt8 { 33 | self[idx.start] 34 | } 35 | 36 | subscript(idx: IntField, endianness endianness: ELF.Endianness) -> UInt16 { 37 | let (a, b) = (UInt16(self[idx.start]), UInt16(self[idx.start + 1])) 38 | 39 | switch endianness { 40 | case .littleEndian: 41 | return a &<< 0 &+ b &<< 8 42 | case .bigEndian: 43 | return a &<< 8 &+ b &<< 0 44 | } 45 | } 46 | } 47 | 48 | /// ELF header 49 | /// 50 | /// - https://en.wikipedia.org/wiki/Executable_and_Linkable_Format 51 | /// - https://refspecs.linuxbase.org/elf/elf.pdf 52 | /// 53 | /// This struct only defines enough fields to identify a valid ELF file 54 | /// and extract the type of object it contains, and the processor 55 | /// architecture and operating system ABI for which that object 56 | /// was created. 57 | struct ELF: Equatable { 58 | /// Minimum ELF header length is 52 bytes for a 32-bit ELF header. 59 | /// A 64-bit header is 64 bytes. A potential header must be at 60 | /// least 52 bytes or it cannot possibly be an ELF header. 61 | static let minHeaderLength = 52 62 | 63 | /// Multibyte ELF fields are stored in the native endianness of the target system. 64 | /// This field records the endianness of objects in the file. 65 | enum Endianness: UInt8 { 66 | case littleEndian = 0x01 67 | case bigEndian = 0x02 68 | } 69 | 70 | /// Offsets (addresses) are stored as 32-bit or 64-bit integers. 71 | /// This field records the offset size used in objects in the file. 72 | /// Variable offset sizes mean that some fields are found at different 73 | /// offsets in 32-bit and 64-bit ELF files. 74 | enum Encoding: UInt8 { 75 | case bits32 = 0x01 76 | case bits64 = 0x02 77 | } 78 | 79 | /// ELF files can hold a variety of different object types. 80 | /// This field records type of object in the file. 81 | /// The standard defines a number of fixed types but also 82 | /// reserves ranges of type numbers for to be used by 83 | /// specific operating systems and processors. 84 | enum Object: Equatable { 85 | case none 86 | case relocatable 87 | case executable 88 | case shared 89 | case core 90 | case reservedOS(UInt16) 91 | case reservedCPU(UInt16) 92 | case unknown(UInt16) 93 | 94 | init?(rawValue: UInt16) { 95 | switch rawValue { 96 | case 0x0000: self = .none 97 | case 0x0001: self = .relocatable 98 | case 0x0002: self = .executable 99 | case 0x0003: self = .shared 100 | case 0x0004: self = .core 101 | 102 | /// Reserved for OS-specific use 103 | case 0xfe00...0xfeff: self = .reservedOS(rawValue) 104 | 105 | /// Reserved for CPU-specific use 106 | case 0xff00...0xffff: self = .reservedCPU(rawValue) 107 | 108 | default: return nil 109 | } 110 | } 111 | } 112 | 113 | /// The ABI used by the object in this ELF file. The standard reserves values for a variety of ABIs and operating systems; only a few are implemented here. 114 | enum ABI: Equatable { 115 | case SysV 116 | case Linux 117 | case unknown(UInt8) 118 | 119 | init(rawValue: UInt8) { 120 | switch rawValue { 121 | case 0x00: self = .SysV 122 | case 0x03: self = .Linux 123 | default: self = .unknown(rawValue) 124 | } 125 | } 126 | } 127 | 128 | /// The processor architecture used by the object in this ELF file. Values are reserved for many ISAs; 129 | /// this enum includes cases for the linux-* host types for which Swift can currently be built: 130 | /// 131 | /// https://github.com/swiftlang/swift/blob/c6d1060778f35631000911372d7645dbd5cade0a/utils/build-script-impl#L458 132 | enum ISA: Equatable { 133 | case x86 134 | case powerpc 135 | case powerpc64 136 | case s390 // incluing s390x 137 | case arm // up to armv7 138 | case x86_64 139 | case aarch64 // armv8 onwards 140 | case riscv 141 | case unknown(UInt16) 142 | 143 | init(rawValue: UInt16) { 144 | switch rawValue { 145 | case 0x0003: self = .x86 146 | case 0x0014: self = .powerpc 147 | case 0x0015: self = .powerpc64 148 | case 0x0016: self = .s390 149 | case 0x0028: self = .arm 150 | case 0x003e: self = .x86_64 151 | case 0x00b7: self = .aarch64 152 | case 0x00f3: self = .riscv 153 | default: self = .unknown(rawValue) 154 | } 155 | } 156 | } 157 | 158 | var encoding: Encoding 159 | var endianness: Endianness 160 | var ABI: ABI 161 | var object: Object 162 | var ISA: ISA 163 | } 164 | 165 | extension ELF { 166 | /// ELF header field addresses 167 | /// 168 | /// The ELF format can store binaries for 32-bit and 64-bit systems, 169 | /// using little-endian and big-endian data encoding. 170 | /// 171 | /// All multibyte fields are stored using the endianness of the target 172 | /// system. Read the EI_DATA field to find the endianness of the file. 173 | /// 174 | /// Some fields are different sizes in 32-bit and 64-bit ELF files, but 175 | /// these occur after all the fields we need to read for basic file type 176 | /// identification, so all our offsets are the same on 32-bit and 64-bit systems. 177 | enum Field { 178 | /// ELF magic number: a string of 4 bytes, not a UInt32; no endianness 179 | static let EI_MAGIC = ArrayField<[UInt8]>(start: 0x0, count: 4) 180 | 181 | /// ELF class (word size): 1 byte 182 | static let EI_CLASS = IntField(start: 0x4) 183 | 184 | /// Data encoding (endianness): 1 byte 185 | static let EI_DATA = IntField(start: 0x5) 186 | 187 | // ELF version: 1 byte 188 | static let EI_VERSION = IntField(start: 0x6) 189 | 190 | // Operating system/ABI identification: 1 byte 191 | static let EI_OSABI = IntField(start: 0x7) 192 | 193 | // The following fields are multibyte, so endianness must be considered, 194 | // All the fields we need are the same length in 32-bit and 64-bit 195 | // ELF files, so their offsets do not change. 196 | 197 | /// Object type: 2 bytes 198 | static let EI_TYPE = IntField(start: 0x10) 199 | 200 | /// Machine ISA (processor architecture): 2 bytes 201 | static let EI_MACHINE = IntField(start: 0x12) 202 | } 203 | 204 | /// The initial magic number (4 bytes) which identifies an ELF file. 205 | /// 206 | /// The ELF magic number is *not* a multibyte integer. It is defined as a 207 | /// string of 4 individual bytes and is the same for little-endian and 208 | /// big-endian ELF files. 209 | static let ELFMagic = Array("\u{7f}ELF".utf8) 210 | 211 | /// Read enough of an ELF header from bytes to discover the object type, 212 | /// processor architecture and operating system ABI. 213 | static func read(_ bytes: [UInt8]) -> ELF? { 214 | // An ELF file starts with a magic number which is the same in either endianness. 215 | // The only defined ELF header version is 1. 216 | guard bytes.count >= minHeaderLength, bytes[Field.EI_MAGIC] == ELFMagic, bytes[Field.EI_VERSION] == 1 else { 217 | return nil 218 | } 219 | 220 | guard 221 | let encoding = Encoding(rawValue: bytes[Field.EI_CLASS]), 222 | let endianness = Endianness(rawValue: bytes[Field.EI_DATA]), 223 | let object = Object(rawValue: bytes[Field.EI_TYPE, endianness: endianness]) 224 | else { 225 | return nil 226 | } 227 | 228 | return ELF( 229 | encoding: encoding, 230 | endianness: endianness, 231 | ABI: .init(rawValue: bytes[Field.EI_OSABI]), 232 | object: object, 233 | ISA: .init(rawValue: bytes[Field.EI_MACHINE, endianness: endianness]) 234 | ) 235 | } 236 | } 237 | 238 | extension ELF { 239 | static func read(at path: URL) throws -> ELF? { 240 | let handle = try FileHandle(forReadingFrom: path) 241 | guard let header = try handle.read(upToCount: minHeaderLength) else { 242 | return nil 243 | } 244 | return ELF.read([UInt8](header)) 245 | } 246 | } 247 | -------------------------------------------------------------------------------- /Sources/containertool/Extensions/Archive+appending.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the SwiftContainerPlugin open source project 4 | // 5 | // Copyright (c) 2025 Apple Inc. and the SwiftContainerPlugin project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of SwiftContainerPlugin project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | import class Foundation.FileManager 16 | import struct Foundation.Data 17 | import struct Foundation.FileAttributeType 18 | import struct Foundation.URL 19 | 20 | import Tar 21 | 22 | extension URL { 23 | var isDirectory: Bool { 24 | (try? resourceValues(forKeys: [.isDirectoryKey]))?.isDirectory == true 25 | } 26 | } 27 | 28 | extension Archive { 29 | /// Append a file or directory tree to the archive. Directory trees are appended recursively. 30 | /// Parameters: 31 | /// - root: The path to the file or directory to add. 32 | /// Returns: A new archive made by appending `root` to the receiver. 33 | public func appendingRecursively(atPath root: String) throws -> Self { 34 | let url = URL(fileURLWithPath: root) 35 | if url.isDirectory { 36 | return try self.appendingDirectoryTree(at: url) 37 | } else { 38 | return try self.appendingFile(at: url) 39 | } 40 | } 41 | 42 | /// Append a single file to the archive. 43 | /// Parameters: 44 | /// - path: The path to the file to add. 45 | /// Returns: A new archive made by appending `path` to the receiver. 46 | func appendingFile(at path: URL) throws -> Self { 47 | try self.appendingFile(name: path.lastPathComponent, data: try [UInt8](Data(contentsOf: path))) 48 | } 49 | 50 | /// Recursively append a single directory tree to the archive. 51 | /// Parameters: 52 | /// - root: The path to the directory to add. 53 | /// Returns: A new archive made by appending `root` to the receiver. 54 | func appendingDirectoryTree(at root: URL) throws -> Self { 55 | var ret = self 56 | 57 | guard let enumerator = FileManager.default.enumerator(atPath: root.path) else { 58 | throw ("Unable to read \(root.path)") 59 | } 60 | 61 | for case let subpath as String in enumerator { 62 | // https://developer.apple.com/documentation/foundation/filemanager/1410452-attributesofitem 63 | // https://developer.apple.com/documentation/foundation/fileattributekey 64 | 65 | guard let filetype = enumerator.fileAttributes?[.type] as? FileAttributeType else { 66 | throw ("Unable to get file type for \(subpath)") 67 | } 68 | 69 | switch filetype { 70 | case .typeRegular: 71 | let resource = try [UInt8](Data(contentsOf: root.appending(path: subpath))) 72 | try ret.appendFile(name: subpath, prefix: root.lastPathComponent, data: resource) 73 | 74 | case .typeDirectory: 75 | try ret.appendDirectory(name: subpath, prefix: root.lastPathComponent) 76 | 77 | default: 78 | throw "Resource file \(subpath) of type \(filetype) is not supported" 79 | } 80 | } 81 | 82 | return ret 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /Sources/containertool/Extensions/ELF+containerArchitecture.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the SwiftContainerPlugin open source project 4 | // 5 | // Copyright (c) 2024 Apple Inc. and the SwiftContainerPlugin project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of SwiftContainerPlugin project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | extension ELF.ISA { 16 | /// Converts the ELF architecture to the GOARCH string representation understood by the container runtime. 17 | /// Unsupported architectures are mapped to nil. 18 | var containerArchitecture: String? { 19 | switch self { 20 | case .x86_64: "amd64" 21 | case .aarch64: "arm64" 22 | default: nil 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Sources/containertool/Extensions/Errors+CustomStringConvertible.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the SwiftContainerPlugin open source project 4 | // 5 | // Copyright (c) 2024 Apple Inc. and the SwiftContainerPlugin project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of SwiftContainerPlugin project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | import ContainerRegistry 16 | 17 | extension HTTPClientError: Swift.CustomStringConvertible { 18 | /// A human-readable string representing an underlying HTTP protocol error 19 | public var description: String { 20 | switch self { 21 | case .unexpectedStatusCode(let status, _, _): 22 | return "Registry returned an unexpected HTTP error code: \(status)" 23 | case .unexpectedContentType(let contentType): 24 | return "Registry returned an unexpected HTTP content type: \(contentType)" 25 | case .missingContentType: return "Registry response did not include a content type" 26 | case .missingResponseHeader(let header): 27 | return "Registry response did not include an expected header: \(header)" 28 | case .authenticationChallenge(let challenge, _, _): 29 | return "Unhandled authentication challenge from registry: \(challenge)" 30 | case .unauthorized: return "Registry response: unauthorized" 31 | } 32 | } 33 | } 34 | 35 | extension DistributionErrorCode: Swift.CustomStringConvertible { 36 | /// A human-readable string representing a distribution protocol error code 37 | public var description: String { self.rawValue } 38 | } 39 | 40 | extension ContainerRegistry.DistributionError: Swift.CustomStringConvertible { 41 | /// A human-readable string describing an unhandled distribution protocol error 42 | public var description: String { if let message { return "\(code): \(message)" } else { return "\(code)" } } 43 | } 44 | 45 | extension ContainerRegistry.DistributionErrors: Swift.CustomStringConvertible { 46 | /// A human-readable string describing a collection of unhandled distribution protocol errors 47 | public var description: String { errors.map { $0.description }.joined(separator: "\n") } 48 | } 49 | 50 | extension ContainerRegistry.ImageReference.Repository.ValidationError: Swift.CustomStringConvertible { 51 | /// A human-readable string describing an image reference validation error 52 | public var description: String { 53 | switch self { 54 | case .emptyString: 55 | return "Invalid reference format: repository name cannot be empty" 56 | case .containsUppercaseLetters(let rawValue): 57 | return "Invalid reference format: repository name (\(rawValue)) must be lowercase" 58 | case .invalidReferenceFormat(let rawValue): 59 | return "Invalid reference format: repository name (\(rawValue)) contains invalid characters" 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /Sources/containertool/Extensions/NetrcError+CustomStringConvertible.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the SwiftContainerPlugin open source project 4 | // 5 | // Copyright (c) 2024 Apple Inc. and the SwiftContainerPlugin project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of SwiftContainerPlugin project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | import Basics 16 | 17 | extension Basics.NetrcError: Swift.CustomStringConvertible { 18 | /// Description of an error in the .netrc file. 19 | public var description: String { 20 | switch self { 21 | case .machineNotFound: return "No entry for host in .netrc" 22 | case .invalidDefaultMachinePosition: return "Invalid .netrc - 'default' must be the last entry in the file" 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Sources/containertool/Extensions/RegistryClient+CopyBlobs.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the SwiftContainerPlugin open source project 4 | // 5 | // Copyright (c) 2024 Apple Inc. and the SwiftContainerPlugin project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of SwiftContainerPlugin project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | import ContainerRegistry 16 | 17 | extension RegistryClient { 18 | /// Copies a blob from another registry to this one. 19 | /// - Parameters: 20 | /// - digest: The digest of the blob to copy. 21 | /// - sourceRepository: The repository from which the blob should be copied. 22 | /// - destClient: The client to which the blob should be copied. 23 | /// - destRepository: The repository on this registry to which the blob should be copied. 24 | /// - Throws: If the copy cannot be completed. 25 | func copyBlob( 26 | digest: String, 27 | fromRepository sourceRepository: ImageReference.Repository, 28 | toClient destClient: RegistryClient, 29 | toRepository destRepository: ImageReference.Repository 30 | ) async throws { 31 | if try await destClient.blobExists(repository: destRepository, digest: digest) { 32 | log("Layer \(digest): already exists") 33 | return 34 | } 35 | 36 | log("Layer \(digest): fetching") 37 | let blob = try await getBlob(repository: sourceRepository, digest: digest) 38 | 39 | log("Layer \(digest): pushing") 40 | let uploaded = try await destClient.putBlob(repository: destRepository, data: blob) 41 | log("Layer \(digest): done") 42 | assert(digest == uploaded.digest) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Sources/containertool/Extensions/RegistryClient+Layers.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the SwiftContainerPlugin open source project 4 | // 5 | // Copyright (c) 2024 Apple Inc. and the SwiftContainerPlugin project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of SwiftContainerPlugin project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | import struct Foundation.Data 16 | import ContainerRegistry 17 | 18 | extension RegistryClient { 19 | func getImageManifest(forImage image: ImageReference, architecture: String) async throws -> ImageManifest { 20 | // We pushed the amd64 tag but it points to a single-architecture index, not directly to a manifest 21 | // if we get an index we should get a manifest, otherwise we might get a manifest directly 22 | 23 | do { 24 | // Try to retrieve a manifest. If the object with this reference is actually an index, the content-type will not match and 25 | // an error will be thrown. 26 | return try await getManifest(repository: image.repository, reference: image.reference) 27 | } catch { 28 | // Try again, treating the top level object as an index. 29 | // This could be more efficient if the exception thrown by getManfiest() included the data it was unable to parse 30 | let index = try await getIndex(repository: image.repository, reference: image.reference) 31 | guard let manifest = index.manifests.first(where: { $0.platform?.architecture == architecture }) else { 32 | throw "Could not find a suitable base image for \(architecture)" 33 | } 34 | // The index should not point to another index; if it does, this call will throw a final error to be handled by the caller. 35 | return try await getManifest(repository: image.repository, reference: manifest.digest) 36 | } 37 | } 38 | 39 | typealias DiffID = String 40 | struct ImageLayer { 41 | var descriptor: ContentDescriptor 42 | var diffID: DiffID 43 | } 44 | 45 | // A layer is a tarball, optionally compressed using gzip or zstd 46 | // See https://github.com/opencontainers/image-spec/blob/main/media-types.md 47 | func uploadLayer( 48 | repository: ImageReference.Repository, 49 | contents: [UInt8], 50 | mediaType: String = "application/vnd.oci.image.layer.v1.tar+gzip" 51 | ) async throws -> ImageLayer { 52 | // The diffID is the hash of the unzipped layer tarball 53 | let diffID = digest(of: contents) 54 | // The layer blob is the gzipped tarball; the descriptor is the hash of this gzipped blob 55 | let blob = Data(gzip(contents)) 56 | let descriptor = try await putBlob(repository: repository, mediaType: mediaType, data: blob) 57 | return ImageLayer(descriptor: descriptor, diffID: diffID) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /Sources/containertool/Logging.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the SwiftContainerPlugin open source project 4 | // 5 | // Copyright (c) 2024 Apple Inc. and the SwiftContainerPlugin project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of SwiftContainerPlugin project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | import Foundation 16 | 17 | /// A text output stream which writes to standard error 18 | struct StdErrOutputStream: TextOutputStream { 19 | /// Writes a string to standard error. 20 | /// - Parameter string: String to be written. 21 | public mutating func write(_ string: String) { fputs(string, stderr) } 22 | } 23 | 24 | /// Logs a message to standard error. 25 | /// - Parameter message: Message to be logged. 26 | public func log(_ message: String) { 27 | var stdError = StdErrOutputStream() 28 | print(message, to: &stdError) 29 | } 30 | -------------------------------------------------------------------------------- /Sources/containertool/gzip.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the SwiftContainerPlugin open source project 4 | // 5 | // Copyright (c) 2024 Apple Inc. and the SwiftContainerPlugin project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of SwiftContainerPlugin project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | //===----------------------------------------------------------------------===// 16 | // 17 | // This source file is part of the SwiftNIO open source project 18 | // 19 | // Copyright (c) 2020-2021 Apple Inc. and the SwiftNIO project authors 20 | // Licensed under Apache License v2.0 21 | // 22 | // See LICENSE.txt for license information 23 | // See CONTRIBUTORS.txt for the list of SwiftNIO project authors 24 | // 25 | // SPDX-License-Identifier: Apache-2.0 26 | // 27 | //===----------------------------------------------------------------------===// 28 | 29 | // Adapted NIOHTTPCompression/HTTPCompression.swift in swift-nio-extras 30 | import VendorCNIOExtrasZlib 31 | 32 | func gzip(_ bytes: [UInt8]) -> [UInt8] { 33 | var stream = z_stream() 34 | stream.zalloc = nil 35 | stream.zfree = nil 36 | stream.opaque = nil 37 | 38 | // Force identical gzip headers to be created on Linux and macOS. 39 | // 40 | // RFC1952 defines operating system codes which can be embedded in the gzip header. 41 | // 42 | // * Initially, zlib generated a default gzip header with the 43 | // OS field set to `Unknown` (255). 44 | // * https://github.com/madler/zlib/commit/0484693e1723bbab791c56f95597bd7dbe867d03 45 | // changed the default to `Unix` (3). 46 | // * https://github.com/madler/zlib/commit/ce12c5cd00628bf8f680c98123a369974d32df15 47 | // changed the default to use a value based on the OS detected 48 | // at compile time. After this, zlib on Linux continued to 49 | // use `Unix` (3) whereas macOS started to use `Apple` (19). 50 | // 51 | // According to RFC1952 Section 2.3.1.2. (Compliance), `Unknown` 52 | // 255 should be used by default where the OS on which the file 53 | // was created is not known. 54 | // 55 | // Different versions of zlib might still produce different 56 | // compressed output for the same input, but using the same default 57 | // value removes one one source of differences between platforms. 58 | 59 | let gz_os_unknown = Int32(255) 60 | var header = gz_header() 61 | header.os = gz_os_unknown 62 | 63 | let windowBits: Int32 = 15 + 16 64 | let level = Z_DEFAULT_COMPRESSION 65 | let memLevel: Int32 = 8 66 | let rc = CNIOExtrasZlib_deflateInit2(&stream, level, Z_DEFLATED, windowBits, memLevel, Z_DEFAULT_STRATEGY) 67 | deflateSetHeader(&stream, &header) 68 | 69 | precondition(rc == Z_OK, "Unexpected return from zlib init: \(rc)") 70 | 71 | var inputBuffer = bytes 72 | 73 | // calculate the upper bound size for the output buffer 74 | let bufferSize = Int(deflateBound(&stream, UInt(inputBuffer.count))) 75 | var outputBuffer: [UInt8] = .init(repeating: 0, count: bufferSize) 76 | 77 | var count = 0 78 | 79 | inputBuffer.withUnsafeMutableBufferPointer { inputPtr in 80 | stream.avail_in = UInt32(inputPtr.count) 81 | stream.next_in = inputPtr.baseAddress! 82 | 83 | outputBuffer.withUnsafeMutableBufferPointer { outputPtr in 84 | stream.avail_out = UInt32(outputPtr.count) 85 | stream.next_out = outputPtr.baseAddress! 86 | 87 | let rc = deflate(&stream, Z_FINISH) 88 | precondition(rc != Z_STREAM_ERROR, "Unexpected return from zlib deflate: \(rc)") 89 | deflateEnd(&stream) 90 | 91 | count = outputPtr.count - Int(stream.avail_out) 92 | } 93 | } 94 | 95 | return Array(outputBuffer[.. Adding the same dependency to your project more than once will prevent it from building. 12 | > If this happens, delete the duplicate dependency lines from `Package.swift` and rebuild. 13 | 14 | Recent versions of `swift package` support the `add-dependency` command: 15 | 16 | ```shell 17 | swift package add-dependency https://github.com/apple/swift-container-plugin --from 1.0.0 18 | ``` 19 | 20 | ### Install the plugin by manually editing `Package.swift` 21 | 22 | If you cannot use the `swift package add-dependency` comand, append the following lines to your project's `Package.swift` file: 23 | 24 | ```swift 25 | package.dependencies += [ 26 | .package(url: "https://github.com/apple/swift-container-plugin", from: "1.0.0"), 27 | ] 28 | ``` 29 | 30 | ### Check that the plugin is available 31 | 32 | After installation, Swift Package Manager should show that the `ContainerImageBuilder` is now available: 33 | 34 | ```shell 35 | % swift package plugin --list 36 | ‘build-container-image’ (plugin ‘ContainerImageBuilder’ in package ‘swift-container-plugin) 37 | ``` 38 | -------------------------------------------------------------------------------- /Sources/swift-container-plugin/Documentation.docc/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 12 | 13 | 14 | CDDefaultCodeListingLanguage 15 | shell 16 | CFBundleName 17 | swift-container-plugin 18 | CFBundleDisplayName 19 | swift-container-plugin 20 | CFBundleIdentifier 21 | com.apple.ContainerPlugin 22 | CFBundlePackageType 23 | DOCS 24 | CDDefaultModuleKind 25 | Tool 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /Sources/swift-container-plugin/Documentation.docc/Swift-Container-Plugin.md: -------------------------------------------------------------------------------- 1 | # Swift Container Plugin 2 | 3 | @Metadata { 4 | @TechnologyRoot() 5 | } 6 | 7 | Build and publish container images using Swift Package Manager. 8 | 9 | ## Overview 10 | 11 | Container images are the standard way to package cloud software today. Once you have packaged your server in a container image, you can deploy it on any container-based public or private cloud service, or run it locally using a desktop container runtime. 12 | 13 | Use Swift Container Plugin to build and publish container images for your Swift services in one streamlined workflow with Swift Package Manager. 14 | 15 | ![Swift Container Plugin flow diagram](swift-container-plugin-flow-diagram) 16 | 17 | 1. [Add the plugin to your project's dependencies](doc:Adding-the-plugin-to-your-project) in `Package.swift`. 18 | 2. [Build and package your service](doc:build) using Swift Package Manager. 19 | - If you are building on macOS, [use a Swift SDK](doc:requirements) to cross-compile a Linux executable. 20 | - If you are building on Linux, use your native Swift compiler to build a Linux executable. If you have special requirements such as building a static executable, or cross-compiling to a different processor architecture, use a suitable Swift SDK. 21 | 3. The plugin automatically packages your executable in a container image and publishes it to your chosen container registry. 22 | 4. [Run your container image](doc:run) on any container-based platform. 23 | 24 | ### Usage 25 | 26 | Swift Container Plugin can package any executable product defined in `Package.swift`. 27 | 28 | #### Build and publish a container image 29 | 30 | After adding the plugin to your project, you can build and publish a container image in one step. 31 | Here is how to build the [HelloWorld example](https://github.com/apple/swift-container-plugin/tree/main/Examples/HelloWorldHummingbird) as a static executable for Linux running on the `x86_64` architecture: 32 | 33 | ``` 34 | % swift package --swift-sdk x86_64-swift-linux-musl \ 35 | build-container-image --repository registry.example.com/myservice 36 | ... 37 | Plugin ‘ContainerImageBuilder’ wants permission to allow all network connections on all ports. 38 | Stated reason: “This command publishes images to container registries over the network”. 39 | Allow this plugin to allow all network connections on all ports? (yes/no) yes 40 | ... 41 | Building for debugging... 42 | Build of product 'containertool' complete! (4.95s) 43 | ... 44 | Build of product 'hello-world' complete! (5.51s) 45 | ... 46 | [ContainerImageBuilder] Found base image manifest: sha256:7bd643386c6e65cbf52f6e2c480b7a76bce8102b562d33ad2aff7c81b7169a42 47 | [ContainerImageBuilder] Found base image configuration: sha256:b904a448fde1f8088913d7ad5121c59645b422e6f94c13d922107f027fb7a5b4 48 | [ContainerImageBuilder] Built application layer 49 | [ContainerImageBuilder] Uploading application layer 50 | [ContainerImageBuilder] Layer sha256:dafa2b0c44d2cfb0be6721f079092ddf15dc8bc537fb07fe7c3264c15cb2e8e6: already exists 51 | [ContainerImageBuilder] Layer sha256:2565d8e736345fc7ba44f9b3900c5c20eda761eee01e01841ac7b494f9db5cf6: already exists 52 | [ContainerImageBuilder] Layer sha256:2c179bb2e4fe6a3b8445fbeb0ce5351cf24817cb0b068c75a219b12434c54a58: already exists 53 | registry.example.com/myservice@sha256:a3f75d0932d052dd9d448a1c9040b16f9f2c2ed9190317147dee95a218faf1df 54 | ``` 55 | 56 | #### Run the image 57 | 58 | Deploy your service in the cloud, or use a standards-compliant container runtime to run it locally: 59 | 60 | ``` 61 | % podman run -p 8080:8080 registry.example.com/myservice@sha256:a3f75d0932d052dd9d448a1c9040b16f9f2c2ed9190317147dee95a218faf1df 62 | Trying to pull registry.example.com/myservice@sha256:a3f75d0932d052dd9d448a1c9040b16f9f2c2ed9190317147dee95a218faf1df... 63 | ... 64 | 2024-05-26T22:57:50+0000 info HummingBird : [HummingbirdCore] Server started and listening on 0.0.0.0:8080 65 | ``` 66 | 67 | ### Find out more 68 | 69 | * Take a look at [more examples](https://github.com/apple/swift-container-plugin/tree/main/Examples). 70 | 71 | * Watch some talks: 72 | 73 | * [How to put Swift in a box](https://fosdem.org/2025/schedule/event/fosdem-2025-5116-how-to-put-swift-in-a-box-building-container-images-with-swift-container-plugin/) at [FOSDEM 2025](https://fosdem.org/2025/schedule/track/swift/). 74 | * [Swift to the cloud in a single step](https://www.youtube.com/watch?v=9AaINsCfZzw) at [ServerSide.Swift 2024](https://www.serversideswift.info/2024/speakers/euan-harris/). 75 | 76 | ## Topics 77 | 78 | ### Essentials 79 | - 80 | - 81 | - 82 | 83 | ### Building and running 84 | - 85 | - 86 | 87 | ### Reference 88 | - 89 | -------------------------------------------------------------------------------- /Sources/swift-container-plugin/Documentation.docc/_Resources/swift-container-plugin-flow-diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apple/swift-container-plugin/bf5e256a84ec856e81100d7eab68217d496254c7/Sources/swift-container-plugin/Documentation.docc/_Resources/swift-container-plugin-flow-diagram.png -------------------------------------------------------------------------------- /Sources/swift-container-plugin/Documentation.docc/authentication.md: -------------------------------------------------------------------------------- 1 | # Set up your registry credentials 2 | 3 | Configure the plugin to authenticate to your container registry. 4 | 5 | ## Overview 6 | 7 | Many container registries require authentication in order to push images, or even pull them. 8 | The plugin reads your registry credentials from a `.netrc` file in your home directory. 9 | Add a record into the `.netrc` file for each registry you use. 10 | The plugin chooses the correct record based on the hostname of the registry's authentication server. 11 | 12 | > For some registries, such as Docker Hub [(see example)](), the authentication server hostname might not be the same as the registry hostname you use when pushing and pulling images. 13 | 14 | The following example shows placeholder values for the registry `registry.example.com`: 15 | 16 | ``` 17 | machine registry.example.com 18 | login myuser 19 | password mypassword 20 | ``` 21 | 22 | The following examples show how to set up the plugin for some popular registry providers. 23 | 24 | ### Docker Hub 25 | 26 | > Don't use your Docker Hub account password to push and pull images. 27 | > Create a Personal Access Token, which has restricted privileges, for each integration you use. 28 | > By using separate tokens, you can monitor them independently and revoke one at any time. 29 | To create a `.netrc` entry for Docker Hub: 30 | 31 | 1. Log into Docker Hub and [generate a Personal Access Token](https://docs.docker.com/security/for-developers/access-tokens/) for Swift Container Plugin. 32 | 33 | 2. **Set the token's access permissions to *Read & Write*.** 34 | 35 | 3. Copy the token and add it, together with your Docker ID, to your `.netrc` file under the machine name `auth.docker.io`: 36 | 37 | The final `.netrc` entry should be similar to this: 38 | 39 | ``` 40 | machine auth.docker.io 41 | login mydockerid 42 | password dckr_pat_B3FwrU... 43 | ``` 44 | 45 | ### GitHub Container Registry 46 | 47 | > GitHub Container Registry only supports authentication using a [Personal Access Token (classic)](https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-container-registry#authenticating-to-the-container-registry). 48 | > A fine-grained personal access token cannot be used. 49 | 50 | To create a `.netrc` entry for Github Container Registry: 51 | 52 | 1. Log into GitHub and [generate a Personal Access Token (classic)](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens#creating-a-personal-access-token-classic) for Swift Container Plugin. 53 | 54 | 2. **Select the *write:packages* scope.** 55 | 56 | 3. Copy the token and add it, together with your GitHub username, to your `.netrc` file: 57 | 58 | The final `.netrc` entry should be similar to this: 59 | 60 | ``` 61 | machine ghcr.io 62 | login mygithubusername 63 | password ghp_fAOsWl... 64 | ``` 65 | 66 | ### Amazon Elastic Container Registry 67 | 68 | > Amazon Elastic Container Registry uses [short-lived authorization tokens](https://docs.aws.amazon.com/AmazonECR/latest/userguide/registry_auth.html#registry-auth-token) which expire after 12 hours. 69 | > 70 | > To generate an ECR authentication token, you must [first install the AWS CLI tools.](https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html) 71 | 72 | To create a `.netrc` entry for Amazon Elastic Container Registry: 73 | 74 | 1. Use the `aws` CLI tool to [generate an authentication token](https://docs.aws.amazon.com/AmazonECR/latest/userguide/registry_auth.html#registry-auth-token). 75 | You'll need to know the name of the [AWS region](https://docs.aws.amazon.com/global-infrastructure/latest/regions/aws-regions.html) in which your registry is hosted. 76 | Registries in different AWS regions are separate and require different authentication tokens. 77 | 78 | For example, the following command generates a token for ECR in the `us-west-2` region: 79 | ``` 80 | aws ecr get-login-password --region us-west-2 81 | ``` 82 | 83 | 2. Copy the token and add it to your `.netrc` file. 84 | * The format of the machine name is: 85 | 86 | ``` 87 | .dkr.ecr..amazonaws.com 88 | ``` 89 | 90 | You can [find your AWS account ID](https://docs.aws.amazon.com/accounts/latest/reference/manage-acct-identifiers.html) in the AWS Management Console or by running the following command: 91 | ``` 92 | aws sts get-caller-identity \ 93 | --query Account \ 94 | --output text 95 | ``` 96 | * **The login name must be `AWS`**. 97 | * The token is a large encoded string. 98 | It must appear in the `.netrc` file as a single line, with no breaks. 99 | 100 | The final `.netrc` entry should be similar to this: 101 | 102 | ``` 103 | machine 123456789012.dkr.ecr.us-west-2.amazonaws.com 104 | login AWS 105 | password eyJwYXlsb2FkIj... 106 | ``` 107 | -------------------------------------------------------------------------------- /Sources/swift-container-plugin/Documentation.docc/build-container-image.md: -------------------------------------------------------------------------------- 1 | # build-container-image plugin 2 | 3 | Wrap a binary in a container image and publish it. 4 | 5 | ## Overview 6 | 7 | `build-container-image` is a Swift Package Manager [command plugin](https://github.com/swiftlang/swift-package-manager/blob/main/Documentation/Plugins.md#using-a-package-plugin) which packages a product defined in `Package.swift` in a container image and publishes it to repository on a container registry. 8 | 9 | ### Usage 10 | 11 | `swift package build-container-image []` 12 | 13 | ### Options 14 | 15 | - term `--product `: 16 | The name of the product to package. 17 | 18 | If `Package.swift` defines only one product, it will be selected by default. 19 | 20 | ### Source and destination repository options 21 | 22 | - term `--default-registry `: 23 | The default registry hostname. (default: `docker.io`) 24 | 25 | If the repository path does not contain a registry hostname, the default registry will be prepended to it. 26 | 27 | - term `--repository `: 28 | Destination image repository. 29 | 30 | If the repository path does not begin with a registry hostname, the default registry will be prepended to the path. 31 | The destination repository must be specified, either by setting the `--repository` option or the `CONTAINERTOOL_REPOSITORY` environment variable. 32 | 33 | - term `--tag `: 34 | The tag to apply to the destination image. 35 | 36 | The `latest` tag is automatically updated to refer to the published image. 37 | 38 | - term `--from `: 39 | Base image reference. (default: `swift:slim`) 40 | 41 | ### Image build options 42 | 43 | - term `--resources `: 44 | Add the file or directory at `resources` to the image. 45 | Directories are added recursively. 46 | 47 | If the `product` being packaged has a [resource bundle](https://developer.apple.com/documentation/xcode/bundling-resources-with-a-swift-package) it will be added to the image automatically. 48 | 49 | ### Image configuration options 50 | 51 | - term `--architecture `: 52 | CPU architecture required to run the image. 53 | 54 | If the base image is `scratch`, the final image will have no base layer and will consist only of the application layer and resource bundle layer, if the product has a resource bundle. 55 | 56 | - term `--os `: 57 | Operating system required to run the image. (default: `linux`) 58 | 59 | ### Authentication options 60 | 61 | - term `--default-username `: 62 | Default username to use when logging into the registry. 63 | 64 | This username is used if there is no matching `.netrc` entry for the registry, there is no `.netrc` file, or the `--disable-netrc` option is set. 65 | The same username is used for the source and destination registries. 66 | 67 | - term `--default-password `: 68 | Default password to use when logging into the registry. 69 | 70 | This password is used if there is no matching `.netrc` entry for the registry, there is no `.netrc` file, or the `--disable-netrc` option is set. 71 | The same password is used for the source and destination registries. 72 | 73 | - term `--enable-netrc/--disable-netrc`: 74 | Load credentials from a netrc file (default: `--enable-netrc`) 75 | 76 | - term `--netrc-file `: 77 | The path to the `.netrc` file. 78 | 79 | - term `--allow-insecure-http `: 80 | Connect to the container registry using plaintext HTTP. (values: `source`, `destination`, `both`) 81 | 82 | ### Options 83 | 84 | - term `-v, --verbose`: 85 | Verbose output. 86 | 87 | - term `-h, --help`: 88 | Show help information. 89 | 90 | ### Environment 91 | 92 | - term `CONTAINERTOOL_DEFAULT_REGISTRY`: 93 | Default image registry hostname, used when the repository path does not contain a registry hostname. 94 | (default: `docker.io`) 95 | 96 | - term `CONTAINERTOOL_REPOSITORY`: 97 | The destination image repository. 98 | 99 | If the path does not begin with a registry hostname, the default registry will be prepended to the path. 100 | The destination repository must be specified, either by setting the `--repository` option or the `CONTAINERTOOL_REPOSITORY` environment variable. 101 | 102 | - term `CONTAINERTOOL_BASE_IMAGE`: 103 | Base image on which to layer the application. 104 | (default: `swift:slim`) 105 | 106 | - term `CONTAINERTOOL_ARCHITECTURE`: 107 | CPU architecture. 108 | 109 | - term `CONTAINERTOOL_OS`: 110 | Operating system. 111 | (default: `Linux`) 112 | 113 | - term `CONTAINERTOOL_DEFAULT_USERNAME`: 114 | Default username to use when logging into the registry. 115 | 116 | This username is used if there is no matching `.netrc` entry for the registry, there is no `.netrc` file, or the `--disable-netrc` option is set. 117 | The same username is used for the source and destination registries. 118 | 119 | - term `CONTAINERTOOL_DEFAULT_PASSWORD`: 120 | Default password to use when logging into the registry. 121 | 122 | This password is used if there is no matching `.netrc` entry for the registry, there is no `.netrc` file, or the `--disable-netrc` option is set. 123 | The same password is used for the source and destination registries. 124 | -------------------------------------------------------------------------------- /Sources/swift-container-plugin/Documentation.docc/build.md: -------------------------------------------------------------------------------- 1 | # Build and package your service 2 | 3 | Build a container image and publish it to a registry. 4 | 5 | ## Overview 6 | 7 | The plugin exposes the command `build-container-image` which you invoke to build your service, package it in a container image and publish it to a container registry, in a single command: 8 | 9 | ```shell 10 | % swift package --swift-sdk x86_64-swift-linux-musl \ 11 | build-container-image --from swift:slim --repository registry.example.com/myservice 12 | ``` 13 | 14 | * The `--swift-sdk` argument specifies the Swift SDK with which to build the executable. In this case we are using the Static Linux SDK, [installed earlier](), to build an statically-linked x86_64 Linux executable. 15 | * The `--from` argument specifies the base image on which our service will run. `swift:slim` is the default, but you can choose your own base image or use `scratch` if your service does not require a base image at all. 16 | * The `--repository` argument specifies the repository to which the plugin will publish the finished image. 17 | 18 | > Note: on macOS, the plugin needs permission to connect to the network to publish the image to the registry. 19 | > 20 | > ``` 21 | > Plugin ‘ContainerImageBuilder’ wants permission to allow all network connections on all ports. 22 | > Stated reason: “This command publishes images to container registries over the network”. 23 | > Allow this plugin to allow all network connections on all ports? (yes/no) 24 | > ``` 25 | > 26 | > Type `yes` to continue. 27 | 28 | ``` 29 | Building for debugging... 30 | Build of product 'containertool' complete! (4.95s) 31 | ... 32 | Build of product 'hello-world' complete! (5.51s) 33 | ... 34 | [ContainerImageBuilder] Found base image manifest: sha256:7bd643386c6e65cbf52f6e2c480b7a76bce8102b562d33ad2aff7c81b7169a42 35 | [ContainerImageBuilder] Found base image configuration: sha256:b904a448fde1f8088913d7ad5121c59645b422e6f94c13d922107f027fb7a5b4 36 | [ContainerImageBuilder] Built application layer 37 | [ContainerImageBuilder] Uploading application layer 38 | [ContainerImageBuilder] Layer sha256:dafa2b0c44d2cfb0be6721f079092ddf15dc8bc537fb07fe7c3264c15cb2e8e6: already exists 39 | [ContainerImageBuilder] Layer sha256:2565d8e736345fc7ba44f9b3900c5c20eda761eee01e01841ac7b494f9db5cf6: already exists 40 | [ContainerImageBuilder] Layer sha256:2c179bb2e4fe6a3b8445fbeb0ce5351cf24817cb0b068c75a219b12434c54a58: already exists 41 | registry.example.com/myservice@sha256:a3f75d0932d052dd9d448a1c9040b16f9f2c2ed9190317147dee95a218faf1df 42 | ``` 43 | 44 | When the plugin finishes, it prints a reference identifying the new image. 45 | Any standard container runtime can use the reference to pull and run your service. 46 | 47 | ### Default registry 48 | 49 | If you don't include a registry name in the `--repository` argument, the plugin will publish your image to Docker Hub by default. 50 | 51 | You can override the default registry by using the `--default-registry` argument or setting the `CONTAINERTOOL_DEFAULT_REGISTRY` environment variable. 52 | 53 | The following examples show how to publish images to some popular registry providers. 54 | 55 | ### Docker Hub 56 | 57 | The following example publishes an image to a repository named `mydockerid/example` on Docker Hub. 58 | 59 | The repository will be created if it does not already exist. 60 | 61 | ``` 62 | swift package --swift-sdk x86_64-swift-linux-musl build-container-image \ 63 | --repository mydockerid/example 64 | ``` 65 | 66 | ### GitHub Container Registry 67 | 68 | The following example publishes an image to a repository named `mydockerid/example` on GitHub Container Registry. 69 | 70 | The repository will be created if it does not already exist. 71 | 72 | ``` 73 | swift package --swift-sdk x86_64-swift-linux-musl build-container-image \ 74 | --repository ghcr.io/mygithubusername/example 75 | ``` 76 | 77 | ### Amazon Elastic Container Registry 78 | 79 | The following example publishes an image to the repository named `example/test` on Amazon Elastic Container Registry. 80 | 81 | **The repository must already exist before you push to it.** 82 | Create a repository using the [Amazon ECR Console](https://docs.aws.amazon.com/AmazonECR/latest/userguide/repository-create.html). 83 | 84 | ``` 85 | swift package --swift-sdk x86_64-swift-linux-musl build-container-image \ 86 | --repository 123456789012.dkr.ecr.us-west-2.amazonaws.com/example/test 87 | ``` 88 | -------------------------------------------------------------------------------- /Sources/swift-container-plugin/Documentation.docc/requirements.md: -------------------------------------------------------------------------------- 1 | # Install a toolchain for cross-compilation 2 | 3 | Install an open source Swift toolchain and a compatible Swift SDK to build Linux executables on macOS. 4 | 5 | ## Overview 6 | 7 | * Swift Container Plugin runs on macOS and Linux and requires Swift 6.0 or later. 8 | * On macOS you must install a cross-compilation Swift SDK, such as the [Swift Static Linux SDK](https://www.swift.org/documentation/articles/static-linux-getting-started.html), in order to build executables which can run on Linux-based cloud infrastructure. 9 | * The Swift Static Linux SDK requires the [open source Swift toolchain](https://www.swift.org/install/macos/) to be installed. 10 | * A container runtime is not required to build an image, but you will need one wherever you want to run the image. 11 | 12 | ### Install an open source Swift toolchain 13 | 14 | Follow the instructions at [https://www.swift.org/install/macos/](https://www.swift.org/install/macos/) to install the open source Swift toolchain. The easiest way to do this is to use the [Swiftly swift toolchain installer](https://www.swift.org/install/macos/swiftly/). 15 | 16 | ### Install a Swift SDK for cross-compilation which matches the open source toolchain 17 | 18 | If you are running on macOS, you can use a [Swift SDK](https://github.com/apple/swift-evolution/blob/main/proposals/0387-cross-compilation-destinations.md) to cross-compile your server executable for Linux. Either: 19 | 20 | * Install the [Static Linux SDK from swift.org](https://www.swift.org/documentation/articles/static-linux-getting-started.html) 21 | * Use [Swift SDK Generator](https://github.com/apple/swift-sdk-generator) to build and install a custom SDK 22 | 23 | The following example illustrates installing the Swift Static Linux SDK: 24 | ``` 25 | % swift sdk install https://download.swift.org/swift-6.1-release/static-sdk/swift-6.1-RELEASE/swift-6.1-RELEASE_static-linux-0.0.1.artifactbundle.tar.gz --checksum 111c6f7d280a651208b8c74c0521dd99365d785c1976a6e23162f55f65379ac6 26 | Downloading a Swift SDK bundle archive from `https://download.swift.org/swift-6.1-release/static-sdk/swift-6.1-RELEASE/swift-6.1-RELEASE_static-linux-0.0.1.artifactbundle.tar.gz`... 27 | Downloading 28 | 100% [=============================================================] 29 | Downloading swift-6.1-RELEASE_static-linux-0.0.1.artifactbundle.tar.gz 30 | 31 | Swift SDK bundle archive successfully downloaded from `https://download.swift.org/swift-6.1-release/static-sdk/swift-6.1-RELEASE/swift-6.1-RELEASE_static-linux-0.0.1.artifactbundle.tar.gz`. 32 | Verifying if checksum of the downloaded archive is valid... 33 | Downloaded archive has a valid checksum. 34 | Swift SDK bundle at `https://download.swift.org/swift-6.1-release/static-sdk/swift-6.1-RELEASE/swift-6.1-RELEASE_static-linux-0.0.1.artifactbundle.tar.gz` is assumed to be an archive, unpacking... 35 | Swift SDK bundle at `https://download.swift.org/swift-6.1-release/static-sdk/swift-6.1-RELEASE/swift-6.1-RELEASE_static-linux-0.0.1.artifactbundle.tar.gz` successfully installed as swift-6.1-RELEASE_static-linux-0.0.1.artifactbundle. 36 | ``` 37 | 38 | After the installation is complete, check that the SDK is available: 39 | 40 | ```shell 41 | % swift sdk list 42 | swift-6.1-RELEASE_static-linux-0.0.1 43 | ``` 44 | -------------------------------------------------------------------------------- /Sources/swift-container-plugin/Documentation.docc/run.md: -------------------------------------------------------------------------------- 1 | # Run your service 2 | 3 | Run the container you built on cloud infrastructure or locally on a desktop container runtime. 4 | ## Overview 5 | 6 | Swift Container Plugin builds standards-compliant container images which can run in public or private cloud infrastructure, or locally on a desktop container runtime. 7 | 8 | The following command uses `podman` to run the service locally, making it available on port 8080: 9 | 10 | ``` 11 | % podman run -p 8080:8080 registry.example.com/myservice@sha256:a3f75d0932d052dd9d448a1c9040b16f9f2c2ed9190317147dee95a218faf1df 12 | Trying to pull registry.example.com/myservice@sha256:a3f75d0932d052dd9d448a1c9040b16f9f2c2ed9190317147dee95a218faf1df... 13 | ... 14 | 2024-05-26T22:57:50+0000 info HummingBird : [HummingbirdCore] Server started and listening on 0.0.0.0:8080 15 | ``` 16 | 17 | When the service has started, we can access it with a web browser or `curl`: 18 | ``` 19 | % curl localhost:8080 20 | Hello World, from Hummingbird on Ubuntu 24.04.2 LTS 21 | ``` 22 | 23 | ### Build and run in one step 24 | 25 | Swift Container Plugin prints a reference to the newly built image on standard output. 26 | You can pipe the image reference to a deployment command or pass it as an argument using shell command substitution. 27 | 28 | This allows you to build and deploy a container image in one shell command, using a convenient pattern offered by tools such as [ko.build](https://ko.build): 29 | 30 | ``` 31 | % podman run -p 8080:8080 \ 32 | $(swift package --swift-sdk x86_64-linux-swift-musl \ 33 | build-container-image --repository registry.example.com/myservice) 34 | Trying to pull registry.example.com/myservice@sha256:a3f75d0932d052dd9d448a1c9040b16f9f2c2ed9190317147dee95a218faf1df... 35 | ... 36 | 2024-05-26T22:57:50+0000 info HummingBird : [HummingbirdCore] Server started and listening on 0.0.0.0:8080 37 | ``` 38 | -------------------------------------------------------------------------------- /Sources/swift-container-plugin/Empty.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the SwiftContainerPlugin open source project 4 | // 5 | // Copyright (c) 2024 Apple Inc. and the SwiftContainerPlugin project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of SwiftContainerPlugin project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | // This is an empty source file used to make SwiftContainerPluginDocumentation a valid 16 | // documentation build target. 17 | 18 | // SwiftContainerPluginDocumentation is an otherwise empty target that includes high-level, 19 | // user-facing documentation about using the Swift Container Plugin from the command-line. 20 | 21 | // To preview the documentation, run the following command: 22 | // 23 | // swift package --disable-sandbox preview-documentation --target swift-container-plugin --analyze 24 | -------------------------------------------------------------------------------- /Sources/swift-container-plugin/README.md: -------------------------------------------------------------------------------- 1 | # Swift Container Plugin User Documentation 2 | 3 | This is an otherwise empty target that includes high-level, user-facing documentation about using the plugin from the command-line. 4 | 5 | To preview the documentation, run the following command: 6 | 7 | ```bash 8 | swift package --disable-sandbox preview-documentation --target swift-container-plugin --analyze 9 | ``` 10 | 11 | 12 | -------------------------------------------------------------------------------- /Tests/ContainerRegistryTests/AuthTests.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the SwiftContainerPlugin open source project 4 | // 5 | // Copyright (c) 2024 Apple Inc. and the SwiftContainerPlugin project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of SwiftContainerPlugin project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | import Basics 16 | import Foundation 17 | import Testing 18 | 19 | struct AuthTests { 20 | // SwiftPM's NetrcAuthorizationProvider does not throw an error if the .netrc file 21 | // does not exist. For simplicity the local vendored version does the same. 22 | @Test func testNonexistentNetrc() async throws { 23 | // Construct a URL to a nonexistent file in the bundle directory 24 | let netrcURL = Bundle.module.resourceURL!.appendingPathComponent("netrc.nonexistent") 25 | #expect(!FileManager.default.fileExists(atPath: netrcURL.path)) 26 | 27 | let authProvider = try NetrcAuthorizationProvider(netrcURL) 28 | #expect(authProvider.authentication(for: URL(string: "https://hub.example.com")!) == nil) 29 | } 30 | 31 | @Test func testEmptyNetrc() async throws { 32 | let netrcURL = Bundle.module.url(forResource: "netrc", withExtension: "empty")! 33 | let authProvider = try NetrcAuthorizationProvider(netrcURL) 34 | #expect(authProvider.authentication(for: URL(string: "https://hub.example.com")!) == nil) 35 | } 36 | 37 | @Test func testBasicNetrc() async throws { 38 | let netrcURL = Bundle.module.url(forResource: "netrc", withExtension: "basic")! 39 | let authProvider = try NetrcAuthorizationProvider(netrcURL) 40 | #expect(authProvider.authentication(for: URL(string: "https://nothing.example.com")!) == nil) 41 | 42 | guard let (user, password) = authProvider.authentication(for: URL(string: "https://hub.example.com")!) else { 43 | Issue.record("Expected to find a username and password") 44 | return 45 | } 46 | #expect(user == "swift") 47 | #expect(password == "password") 48 | } 49 | 50 | // The default entry is used if no specific entry matches 51 | @Test func testComplexNetrcWithDefault() async throws { 52 | let netrcURL = Bundle.module.url(forResource: "netrc", withExtension: "default")! 53 | let authProvider = try NetrcAuthorizationProvider(netrcURL) 54 | 55 | guard let (user, password) = authProvider.authentication(for: URL(string: "https://nothing.example.com")!) 56 | else { 57 | Issue.record("Expected to find a username and password") 58 | return 59 | } 60 | #expect(user == "defaultlogin") 61 | #expect(password == "defaultpassword") 62 | } 63 | 64 | // The default entry must be last in the file 65 | @Test func testComplexNetrcWithInvalidDefault() async throws { 66 | let netrcURL = Bundle.module.url(forResource: "netrc", withExtension: "invaliddefault")! 67 | #expect { try NetrcAuthorizationProvider(netrcURL) } throws: { error in 68 | error as! NetrcError == NetrcError.invalidDefaultMachinePosition 69 | } 70 | } 71 | 72 | // If there are multiple entries for the same host, the last one wins 73 | @Test func testComplexNetrcOverriddenEntry() async throws { 74 | let netrcURL = Bundle.module.url(forResource: "netrc", withExtension: "default")! 75 | let authProvider = try NetrcAuthorizationProvider(netrcURL) 76 | 77 | guard let (user, password) = authProvider.authentication(for: URL(string: "https://hub.example.com")!) else { 78 | Issue.record("Expected to find a username and password") 79 | return 80 | } 81 | #expect(user == "swift2") 82 | #expect(password == "password2") 83 | } 84 | 85 | // A singleton entry in a netrc file with defaults and overriden entries continues to work as in the simple case 86 | @Test func testComplexNetrcSingletonEntry() async throws { 87 | let netrcURL = Bundle.module.url(forResource: "netrc", withExtension: "default")! 88 | let authProvider = try NetrcAuthorizationProvider(netrcURL) 89 | 90 | guard let (user, password) = authProvider.authentication(for: URL(string: "https://another.example.com")!) 91 | else { 92 | Issue.record("Expected to find a username and password") 93 | return 94 | } 95 | #expect(user == "anotherlogin") 96 | #expect(password == "anotherpassword") 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /Tests/ContainerRegistryTests/ImageReferenceTests.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the SwiftContainerPlugin open source project 4 | // 5 | // Copyright (c) 2024 Apple Inc. and the SwiftContainerPlugin project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of SwiftContainerPlugin project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | @testable import ContainerRegistry 16 | import Testing 17 | 18 | struct ReferenceTestCase: Sendable { 19 | var reference: String 20 | var expected: ImageReference 21 | } 22 | 23 | struct ReferenceTests { 24 | static let tests = [ 25 | // A reference which does not contain a '/' is always interpreted as a repository name 26 | // in the default registry. 27 | ReferenceTestCase( 28 | reference: "localhost", 29 | expected: try! ImageReference( 30 | registry: "default", 31 | repository: ImageReference.Repository("localhost"), 32 | reference: "latest" 33 | ) 34 | ), 35 | ReferenceTestCase( 36 | reference: "example.com", 37 | expected: try! ImageReference( 38 | registry: "default", 39 | repository: ImageReference.Repository("example.com"), 40 | reference: "latest" 41 | ) 42 | ), 43 | ReferenceTestCase( 44 | reference: "example:1234", 45 | expected: try! ImageReference( 46 | registry: "default", 47 | repository: ImageReference.Repository("example"), 48 | reference: "1234" 49 | ) 50 | ), 51 | 52 | // If a reference contains a '/' *and* the component before the '/' looks like a 53 | // hostname, the part before the '/' is interpreted as a registry and the part after 54 | // the '/' is used as a repository. 55 | // 56 | // In general a hostname must have at least two dot-separated components. 57 | // "localhost" is a special case. 58 | ReferenceTestCase( 59 | reference: "localhost/foo", 60 | expected: try! ImageReference( 61 | registry: "localhost", 62 | repository: ImageReference.Repository("foo"), 63 | reference: "latest" 64 | ) 65 | ), 66 | ReferenceTestCase( 67 | reference: "localhost:1234/foo", 68 | expected: try! ImageReference( 69 | registry: "localhost:1234", 70 | repository: ImageReference.Repository("foo"), 71 | reference: "latest" 72 | ) 73 | ), 74 | ReferenceTestCase( 75 | reference: "example.com/foo", 76 | expected: try! ImageReference( 77 | registry: "example.com", 78 | repository: ImageReference.Repository("foo"), 79 | reference: "latest" 80 | ) 81 | ), 82 | ReferenceTestCase( 83 | reference: "example.com:1234/foo", 84 | expected: try! ImageReference( 85 | registry: "example.com:1234", 86 | repository: ImageReference.Repository("foo"), 87 | reference: "latest" 88 | ) 89 | ), 90 | ReferenceTestCase( 91 | reference: "example.com:1234/foo:bar", 92 | expected: try! ImageReference( 93 | registry: "example.com:1234", 94 | repository: ImageReference.Repository("foo"), 95 | reference: "bar" 96 | ) 97 | ), 98 | 99 | // If the part before the '/' does not look like a hostname, the whole reference 100 | // is interpreted as a repository name in the default registry. 101 | ReferenceTestCase( 102 | reference: "local/foo", 103 | expected: try! ImageReference( 104 | registry: "default", 105 | repository: ImageReference.Repository("local/foo"), 106 | reference: "latest" 107 | ) 108 | ), 109 | ReferenceTestCase( 110 | reference: "example/foo", 111 | expected: try! ImageReference( 112 | registry: "default", 113 | repository: ImageReference.Repository("example/foo"), 114 | reference: "latest" 115 | ) 116 | ), 117 | ReferenceTestCase( 118 | reference: "example/foo:1234", 119 | expected: try! ImageReference( 120 | registry: "default", 121 | repository: ImageReference.Repository("example/foo"), 122 | reference: "1234" 123 | ) 124 | ), 125 | 126 | // Distribution spec tests 127 | ReferenceTestCase( 128 | reference: "example.com/foo@sha256:0123456789abcdef01234567890abcdef", 129 | expected: try! ImageReference( 130 | registry: "example.com", 131 | repository: ImageReference.Repository("foo"), 132 | reference: "sha256:0123456789abcdef01234567890abcdef" 133 | ) 134 | ), 135 | 136 | // This example goes against the distribution spec's regular expressions but matches observed client behaviour 137 | ReferenceTestCase( 138 | reference: "foo:1234/bar:1234", 139 | expected: try! ImageReference( 140 | registry: "foo:1234", 141 | repository: ImageReference.Repository("bar"), 142 | reference: "1234" 143 | ) 144 | ), 145 | ReferenceTestCase( 146 | reference: "localhost/foo:1234/bar:1234", 147 | expected: try! ImageReference( 148 | registry: "localhost", 149 | repository: ImageReference.Repository("foo"), 150 | reference: "1234/bar:1234" 151 | ) 152 | ), 153 | 154 | // Capitals are not allowed in repository names but are allowed in hostnames (matching podman's behaviour) 155 | ReferenceTestCase( 156 | reference: "EXAMPLE.COM/foo:latest", 157 | expected: try! ImageReference( 158 | registry: "EXAMPLE.COM", 159 | repository: ImageReference.Repository("foo"), 160 | reference: "latest" 161 | ) 162 | ), 163 | ] 164 | 165 | @Test(arguments: tests) 166 | func testValidReferences(test: ReferenceTestCase) throws { 167 | let parsed = try! ImageReference(fromString: test.reference, defaultRegistry: "default") 168 | #expect( 169 | parsed == test.expected, 170 | "\(String(reflecting: parsed)) is not equal to \(String(reflecting: test.expected))" 171 | ) 172 | } 173 | 174 | @Test 175 | func testInvalidReferences() throws { 176 | #expect(throws: ImageReference.Repository.ValidationError.emptyString) { 177 | try ImageReference(fromString: "", defaultRegistry: "default") 178 | } 179 | 180 | #expect(throws: ImageReference.Repository.ValidationError.emptyString) { 181 | try ImageReference(fromString: "example.com/") 182 | } 183 | 184 | #expect(throws: ImageReference.Repository.ValidationError.containsUppercaseLetters("helloWorld")) { 185 | try ImageReference(fromString: "helloWorld", defaultRegistry: "default") 186 | } 187 | 188 | #expect(throws: ImageReference.Repository.ValidationError.containsUppercaseLetters("helloWorld")) { 189 | try ImageReference(fromString: "localhost:5555/helloWorld") 190 | } 191 | 192 | #expect(throws: ImageReference.Repository.ValidationError.invalidReferenceFormat("hello^world")) { 193 | try ImageReference(fromString: "localhost:5555/hello^world") 194 | } 195 | } 196 | 197 | @Test 198 | func testLibraryReferences() throws { 199 | // docker.io is a special case, as references such as "swift:slim" with no registry component are translated to "docker.io/library/swift:slim" 200 | // Verified against the behaviour of the docker CLI client 201 | 202 | // Fully-qualified name splits as usual 203 | #expect( 204 | try! ImageReference(fromString: "docker.io/library/swift:slim", defaultRegistry: "docker.io") 205 | == ImageReference( 206 | registry: "index.docker.io", 207 | repository: ImageReference.Repository("library/swift"), 208 | reference: "slim" 209 | ) 210 | ) 211 | 212 | // A repository with no '/' part is assumed to be `library` 213 | #expect( 214 | try! ImageReference(fromString: "docker.io/swift:slim", defaultRegistry: "docker.io") 215 | == ImageReference( 216 | registry: "index.docker.io", 217 | repository: ImageReference.Repository("library/swift"), 218 | reference: "slim" 219 | ) 220 | ) 221 | 222 | // Parsing with 'docker.io' as default registry is the same as the fully qualified case 223 | #expect( 224 | try! ImageReference(fromString: "library/swift:slim", defaultRegistry: "docker.io") 225 | == ImageReference( 226 | registry: "index.docker.io", 227 | repository: ImageReference.Repository("library/swift"), 228 | reference: "slim" 229 | ) 230 | ) 231 | 232 | // Bare image name with no registry or repository is interpreted as being in docker.io/library when default is docker.io 233 | #expect( 234 | try! ImageReference(fromString: "swift:slim", defaultRegistry: "docker.io") 235 | == ImageReference( 236 | registry: "index.docker.io", 237 | repository: ImageReference.Repository("library/swift"), 238 | reference: "slim" 239 | ) 240 | ) 241 | 242 | // The minimum reference to a library image. No tag implies `latest` 243 | #expect( 244 | try! ImageReference(fromString: "swift", defaultRegistry: "docker.io") 245 | == ImageReference( 246 | registry: "index.docker.io", 247 | repository: ImageReference.Repository("library/swift"), 248 | reference: "latest" 249 | ) 250 | ) 251 | 252 | // If the registry is not docker.io, the special case logic for `library` does not apply 253 | #expect( 254 | try! ImageReference(fromString: "localhost:5000/swift", defaultRegistry: "docker.io") 255 | == ImageReference( 256 | registry: "localhost:5000", 257 | repository: ImageReference.Repository("swift"), 258 | reference: "latest" 259 | ) 260 | ) 261 | 262 | #expect( 263 | try! ImageReference(fromString: "swift", defaultRegistry: "localhost:5000") 264 | == ImageReference( 265 | registry: "localhost:5000", 266 | repository: ImageReference.Repository("swift"), 267 | reference: "latest" 268 | ) 269 | ) 270 | } 271 | } 272 | -------------------------------------------------------------------------------- /Tests/ContainerRegistryTests/Resources/netrc.basic: -------------------------------------------------------------------------------- 1 | machine hub.example.com 2 | login swift 3 | password password 4 | -------------------------------------------------------------------------------- /Tests/ContainerRegistryTests/Resources/netrc.default: -------------------------------------------------------------------------------- 1 | machine hub.example.com 2 | login swift 3 | password password 4 | 5 | machine another.example.com 6 | login anotherlogin 7 | password anotherpassword 8 | 9 | machine hub.example.com 10 | login swift2 11 | password password2 12 | 13 | default 14 | login defaultlogin 15 | password defaultpassword 16 | 17 | -------------------------------------------------------------------------------- /Tests/ContainerRegistryTests/Resources/netrc.empty: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apple/swift-container-plugin/bf5e256a84ec856e81100d7eab68217d496254c7/Tests/ContainerRegistryTests/Resources/netrc.empty -------------------------------------------------------------------------------- /Tests/ContainerRegistryTests/Resources/netrc.invaliddefault: -------------------------------------------------------------------------------- 1 | default 2 | login defaultlogin 3 | password defaultpassword 4 | 5 | machine hub.example.com 6 | login swift 7 | password password 8 | 9 | machine another.example.com 10 | login anotherlogin 11 | password anotherpassword 12 | 13 | machine hub.example.com 14 | login swift2 15 | password password2 16 | 17 | -------------------------------------------------------------------------------- /Tests/ContainerRegistryTests/SmokeTests.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the SwiftContainerPlugin open source project 4 | // 5 | // Copyright (c) 2024 Apple Inc. and the SwiftContainerPlugin project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of SwiftContainerPlugin project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | import class Foundation.ProcessInfo 16 | @testable import ContainerRegistry 17 | import Testing 18 | 19 | struct SmokeTests { 20 | // These are basic tests to exercise the main registry operations. 21 | // The tests assume that a fresh, empty registry instance is available at 22 | // http://$REGISTRY_HOST:$REGISTRY_PORT 23 | 24 | var client: RegistryClient 25 | let registryHost = ProcessInfo.processInfo.environment["REGISTRY_HOST"] ?? "localhost" 26 | let registryPort = ProcessInfo.processInfo.environment["REGISTRY_PORT"] ?? "5000" 27 | 28 | /// Registry client fixture created for each test 29 | init() async throws { 30 | client = try await RegistryClient(registry: "\(registryHost):\(registryPort)", insecure: true) 31 | } 32 | 33 | @Test func testGetTags() async throws { 34 | let repository = try ImageReference.Repository("testgettags") 35 | 36 | // registry:2 does not validate the contents of the config or image blobs 37 | // so a smoke test can use simple data. Other registries are not so forgiving. 38 | let config_descriptor = try await client.putBlob( 39 | repository: repository, 40 | mediaType: "application/vnd.docker.container.image.v1+json", 41 | data: "testconfiguration".data(using: .utf8)! 42 | ) 43 | 44 | // Initially there will be no tags 45 | do { 46 | _ = try await client.getTags(repository: repository) 47 | Issue.record("Getting tags for an untagged blob should have thrown an error") 48 | } catch { 49 | // Expect to receive an error 50 | } 51 | 52 | // We need to create a manifest referring to the blob, which can then be tagged 53 | let test_manifest = ImageManifest( 54 | schemaVersion: 2, 55 | mediaType: "application/vnd.oci.image.manifest.v1+json", 56 | config: config_descriptor, 57 | layers: [] 58 | ) 59 | 60 | // After setting a tag, we should be able to retrieve it 61 | let _ = try await client.putManifest(repository: repository, reference: "latest", manifest: test_manifest) 62 | let firstTag = try await client.getTags(repository: repository).tags.sorted() 63 | #expect(firstTag == ["latest"]) 64 | 65 | // After setting another tag, the original tag should still exist 66 | let _ = try await client.putManifest( 67 | repository: repository, 68 | reference: "additional_tag", 69 | manifest: test_manifest 70 | ) 71 | let secondTag = try await client.getTags(repository: repository) 72 | #expect(secondTag.tags.sorted() == ["additional_tag", "latest"].sorted()) 73 | } 74 | 75 | @Test func testGetNonexistentBlob() async throws { 76 | let repository = try ImageReference.Repository("testgetnonexistentblob") 77 | 78 | do { 79 | let _ = try await client.getBlob( 80 | repository: repository, 81 | digest: "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" 82 | ) 83 | Issue.record("should have thrown") 84 | } catch {} 85 | } 86 | 87 | @Test func testCheckNonexistentBlob() async throws { 88 | let repository = try ImageReference.Repository("testchecknonexistentblob") 89 | 90 | let exists = try await client.blobExists( 91 | repository: repository, 92 | digest: "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" 93 | ) 94 | #expect(!exists) 95 | } 96 | 97 | @Test func testPutAndGetBlob() async throws { 98 | let repository = try ImageReference.Repository("testputandgetblob") 99 | 100 | let blob_data = "test".data(using: .utf8)! 101 | 102 | let descriptor = try await client.putBlob(repository: repository, data: blob_data) 103 | #expect(descriptor.digest == "sha256:9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08") 104 | 105 | let exists = try await client.blobExists(repository: repository, digest: descriptor.digest) 106 | #expect(exists) 107 | 108 | let blob = try await client.getBlob(repository: repository, digest: descriptor.digest) 109 | #expect(blob == blob_data) 110 | } 111 | 112 | @Test func testPutAndGetTaggedManifest() async throws { 113 | let repository = try ImageReference.Repository("testputandgettaggedmanifest") 114 | 115 | // registry:2 does not validate the contents of the config or image blobs 116 | // so a smoke test can use simple data. Other registries are not so forgiving. 117 | let config_data = "configuration".data(using: .utf8)! 118 | let config_descriptor = try await client.putBlob( 119 | repository: repository, 120 | mediaType: "application/vnd.docker.container.image.v1+json", 121 | data: config_data 122 | ) 123 | 124 | let image_data = "image_layer".data(using: .utf8)! 125 | let image_descriptor = try await client.putBlob( 126 | repository: repository, 127 | mediaType: "application/vnd.docker.image.rootfs.diff.tar.gzip", 128 | data: image_data 129 | ) 130 | 131 | let test_manifest = ImageManifest( 132 | schemaVersion: 2, 133 | mediaType: "application/vnd.oci.image.manifest.v1+json", 134 | config: config_descriptor, 135 | layers: [image_descriptor] 136 | ) 137 | 138 | let _ = try await client.putManifest(repository: repository, reference: "latest", manifest: test_manifest) 139 | 140 | let manifest = try await client.getManifest(repository: repository, reference: "latest") 141 | #expect(manifest.schemaVersion == 2) 142 | #expect(manifest.config.mediaType == "application/vnd.docker.container.image.v1+json") 143 | #expect(manifest.layers.count == 1) 144 | #expect(manifest.layers[0].mediaType == "application/vnd.docker.image.rootfs.diff.tar.gzip") 145 | } 146 | 147 | @Test func testPutAndGetAnonymousManifest() async throws { 148 | let repository = try ImageReference.Repository("testputandgetanonymousmanifest") 149 | 150 | // registry:2 does not validate the contents of the config or image blobs 151 | // so a smoke test can use simple data. Other registries are not so forgiving. 152 | let config_data = "configuration".data(using: .utf8)! 153 | let config_descriptor = try await client.putBlob( 154 | repository: repository, 155 | mediaType: "application/vnd.docker.container.image.v1+json", 156 | data: config_data 157 | ) 158 | 159 | let image_data = "image_layer".data(using: .utf8)! 160 | let image_descriptor = try await client.putBlob( 161 | repository: repository, 162 | mediaType: "application/vnd.docker.image.rootfs.diff.tar.gzip", 163 | data: image_data 164 | ) 165 | 166 | let test_manifest = ImageManifest( 167 | schemaVersion: 2, 168 | mediaType: "application/vnd.oci.image.manifest.v1+json", 169 | config: config_descriptor, 170 | layers: [image_descriptor] 171 | ) 172 | 173 | let _ = try await client.putManifest( 174 | repository: repository, 175 | reference: test_manifest.digest, 176 | manifest: test_manifest 177 | ) 178 | 179 | let manifest = try await client.getManifest(repository: repository, reference: test_manifest.digest) 180 | #expect(manifest.schemaVersion == 2) 181 | #expect(manifest.config.mediaType == "application/vnd.docker.container.image.v1+json") 182 | #expect(manifest.layers.count == 1) 183 | #expect(manifest.layers[0].mediaType == "application/vnd.docker.image.rootfs.diff.tar.gzip") 184 | } 185 | 186 | @Test func testPutAndGetImageConfiguration() async throws { 187 | let repository = try ImageReference.Repository("testputandgetimageconfiguration") 188 | let image = ImageReference(registry: "registry", repository: repository, reference: "latest") 189 | 190 | let configuration = ImageConfiguration( 191 | created: "1996-12-19T16:39:57-08:00", 192 | author: "test", 193 | architecture: "x86_64", 194 | os: "Linux", 195 | rootfs: .init(_type: "layers", diff_ids: ["abc123", "def456"]), 196 | history: [.init(created: "1996-12-19T16:39:57-08:00", author: "test", created_by: "smoketest")] 197 | ) 198 | let config_descriptor = try await client.putImageConfiguration( 199 | forImage: image, 200 | configuration: configuration 201 | ) 202 | 203 | let downloaded = try await client.getImageConfiguration( 204 | forImage: image, 205 | digest: config_descriptor.digest 206 | ) 207 | 208 | #expect(configuration == downloaded) 209 | } 210 | } 211 | -------------------------------------------------------------------------------- /Tests/containertoolTests/ELFDetectTests.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the SwiftContainerPlugin open source project 4 | // 5 | // Copyright (c) 2025 Apple Inc. and the SwiftContainerPlugin project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of SwiftContainerPlugin project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | import Foundation 16 | import Testing 17 | 18 | @testable import containertool 19 | 20 | @Suite struct ElfDetectUnitTests { 21 | let invalid_magic: [UInt8] = [ 22 | 0x12, 0x34, 0x4c, 0x46, 0x02, 0x01, 0x42, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 23 | 0x02, 0x00, 0x3e, 0x00, 0x01, 0x00, 0x00, 0x00, 0x10, 0xa9, 0x31, 0x00, 0x00, 0x00, 0x00, 0x00, 24 | 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x28, 0x88, 0x8a, 0x02, 0x00, 0x00, 0x00, 0x00, 25 | 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x38, 0x00, 0x09, 0x00, 0x40, 0x00, 0x32, 0x00, 0x30, 0x00, 26 | ] 27 | 28 | let invalid_version: [UInt8] = [ 29 | 0x7f, 0x45, 0x4c, 0x46, 0x02, 0x01, 0x42, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 30 | 0x02, 0x00, 0x3e, 0x00, 0x01, 0x00, 0x00, 0x00, 0x10, 0xa9, 0x31, 0x00, 0x00, 0x00, 0x00, 0x00, 31 | 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x28, 0x88, 0x8a, 0x02, 0x00, 0x00, 0x00, 0x00, 32 | 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x38, 0x00, 0x09, 0x00, 0x40, 0x00, 0x32, 0x00, 0x30, 0x00, 33 | ] 34 | 35 | let invalid_length: [UInt8] = [ 36 | 0x7f, 0x45, 0x4c, 0x46, 0x02, 37 | ] 38 | 39 | // x86-64 executable (64-bit, little-endian) 40 | let linux_x86_64: [UInt8] = [ 41 | 0x7f, 0x45, 0x4c, 0x46, 0x02, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 42 | 0x02, 0x00, 0x3e, 0x00, 0x01, 0x00, 0x00, 0x00, 0x10, 0xa9, 0x31, 0x00, 0x00, 0x00, 0x00, 0x00, 43 | 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x28, 0x88, 0x8a, 0x02, 0x00, 0x00, 0x00, 0x00, 44 | 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x38, 0x00, 0x09, 0x00, 0x40, 0x00, 0x32, 0x00, 0x30, 0x00, 45 | ] 46 | 47 | // AArch64 executable (64-bit, little-endian) 48 | let linux_aarch64: [UInt8] = [ 49 | 0x7f, 0x45, 0x4c, 0x46, 0x02, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 50 | 0x02, 0x00, 0xb7, 0x00, 0x01, 0x00, 0x00, 0x00, 0xe0, 0x90, 0x2f, 0x00, 0x00, 0x00, 0x00, 0x00, 51 | 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc0, 0x8a, 0x82, 0x02, 0x00, 0x00, 0x00, 0x00, 52 | 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x38, 0x00, 0x09, 0x00, 0x40, 0x00, 0x32, 0x00, 0x30, 0x00, 53 | ] 54 | 55 | // AArch64 core (64-bit, little-endian) 56 | let linux_aarch64_core: [UInt8] = [ 57 | 0x7f, 0x45, 0x4c, 0x46, 0x02, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 58 | 0x04, 0x00, 0xb7, 0x00, 0x01, 0x00, 0x00, 0x00, 0xe0, 0x90, 0x2f, 0x00, 0x00, 0x00, 0x00, 0x00, 59 | 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc0, 0x8a, 0x82, 0x02, 0x00, 0x00, 0x00, 0x00, 60 | 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x38, 0x00, 0x09, 0x00, 0x40, 0x00, 0x32, 0x00, 0x30, 0x00, 61 | ] 62 | 63 | // AArch64 os-specific (64-bit, little-endian) 64 | let linux_aarch64_os_specific: [UInt8] = [ 65 | 0x7f, 0x45, 0x4c, 0x46, 0x02, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 66 | 0xff, 0xfe, 0xb7, 0x00, 0x01, 0x00, 0x00, 0x00, 0xe0, 0x90, 0x2f, 0x00, 0x00, 0x00, 0x00, 0x00, 67 | 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc0, 0x8a, 0x82, 0x02, 0x00, 0x00, 0x00, 0x00, 68 | 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x38, 0x00, 0x09, 0x00, 0x40, 0x00, 0x32, 0x00, 0x30, 0x00, 69 | ] 70 | 71 | // PowerPC64 is one of the small number of practical current big endian architectures. 72 | 73 | // PowerPC64 executable (64-bit, big-endian) 74 | let linux_ppc64: [UInt8] = [ 75 | 0x7f, 0x45, 0x4c, 0x46, 0x02, 0x02, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 76 | 0x00, 0x02, 0x00, 0x15, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x31, 0xd0, 77 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x90, 78 | 0x00, 0x00, 0x00, 0x01, 0x00, 0x40, 0x00, 0x38, 0x00, 0x06, 0x00, 0x40, 0x00, 0x17, 0x00, 0x14, 79 | ] 80 | 81 | // PowerPC64le executable (64-bit, little-endian) 82 | let linux_ppc64le: [UInt8] = [ 83 | 0x7f, 0x45, 0x4c, 0x46, 0x02, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 84 | 0x02, 0x00, 0x15, 0x00, 0x01, 0x00, 0x00, 0x00, 0x50, 0x50, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 85 | 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x90, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 86 | 0x02, 0x00, 0x00, 0x00, 0x40, 0x00, 0x38, 0x00, 0x06, 0x00, 0x40, 0x00, 0x17, 0x00, 0x14, 0x00, 87 | ] 88 | 89 | // MIPS64 is not a current Swift build target but can be used to test handling of unknown ISAs. 90 | 91 | // MIPS64 executable (64-bit, big-endian) 92 | let linux_mips64: [UInt8] = [ 93 | 0x7f, 0x45, 0x4c, 0x46, 0x02, 0x02, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 94 | 0x00, 0x02, 0x00, 0x08, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0a, 0x3f, 0x58, 95 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x90, 96 | 0x20, 0x00, 0x00, 0x04, 0x00, 0x40, 0x00, 0x38, 0x00, 0x06, 0x00, 0x40, 0x00, 0x17, 0x00, 0x14, 97 | ] 98 | 99 | // MIPS64le executable (64-bit, little-endian) 100 | let linux_mips64le: [UInt8] = [ 101 | 0x7f, 0x45, 0x4c, 0x46, 0x02, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 102 | 0x02, 0x00, 0x08, 0x00, 0x01, 0x00, 0x00, 0x00, 0x80, 0x3b, 0x0a, 0x00, 0x00, 0x00, 0x00, 0x00, 103 | 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x90, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 104 | 0x04, 0x00, 0x00, 0x20, 0x40, 0x00, 0x38, 0x00, 0x06, 0x00, 0x40, 0x00, 0x17, 0x00, 0x14, 0x00, 105 | ] 106 | 107 | @Test func testReadInvalidMagic() async throws { 108 | // Verify test data: 64-bit ELF headers should be 64 bytes long 109 | #expect(invalid_magic.count == 0x40) 110 | 111 | #expect(ELF.read(invalid_magic) == nil) 112 | } 113 | 114 | @Test func testReadInvalidVersion() async throws { 115 | // Verify test data: 64-bit ELF headers should be 64 bytes long 116 | #expect(invalid_version.count == 0x40) 117 | 118 | #expect(ELF.read(invalid_version) == nil) 119 | } 120 | 121 | @Test func testReadInvalidLength() async throws { 122 | #expect(ELF.read(invalid_length) == nil) 123 | } 124 | 125 | @Test func testReadLinuxX86_64() async throws { 126 | // Verify test data: 64-bit ELF headers should be 64 bytes long 127 | #expect(linux_x86_64.count == 0x40) 128 | 129 | // Compare to values from readelf 130 | let header = try #require(ELF.read(linux_x86_64)) 131 | let expected = ELF( 132 | encoding: .bits64, 133 | endianness: .littleEndian, 134 | ABI: .SysV, 135 | object: .executable, 136 | ISA: .x86_64 137 | ) 138 | #expect(header == expected) 139 | } 140 | 141 | @Test func testReadLinuxAarch64() async throws { 142 | // Verify test data: 64-bit ELF headers should be 64 bytes long 143 | #expect(linux_aarch64.count == 0x40) 144 | 145 | // Compare to values from readelf 146 | let header = try #require(ELF.read(linux_aarch64)) 147 | let expected = ELF( 148 | encoding: .bits64, 149 | endianness: .littleEndian, 150 | ABI: .SysV, 151 | object: .executable, 152 | ISA: .aarch64 153 | ) 154 | #expect(header == expected) 155 | } 156 | 157 | @Test func testReadLinuxAarch64Core() async throws { 158 | // Verify test data: 64-bit ELF headers should be 64 bytes long 159 | #expect(linux_aarch64_core.count == 0x40) 160 | 161 | // Compare to values from readelf 162 | let header = try #require(ELF.read(linux_aarch64_core)) 163 | let expected = ELF( 164 | encoding: .bits64, 165 | endianness: .littleEndian, 166 | ABI: .SysV, 167 | object: .core, 168 | ISA: .aarch64 169 | ) 170 | #expect(header == expected) 171 | } 172 | 173 | @Test func testReadLinuxAarch64OSSpecific() async throws { 174 | // Verify test data: 64-bit ELF headers should be 64 bytes long 175 | #expect(linux_aarch64_os_specific.count == 0x40) 176 | 177 | // Compare to values from readelf 178 | let header = try #require(ELF.read(linux_aarch64_os_specific)) 179 | let expected = ELF( 180 | encoding: .bits64, 181 | endianness: .littleEndian, 182 | ABI: .SysV, 183 | object: .reservedOS(0xfeff), // Upper bound of OS-specific range 184 | ISA: .aarch64 185 | ) 186 | #expect(header == expected) 187 | } 188 | 189 | @Test func testReadLinuxPPC64() async throws { 190 | // Verify test data: 64-bit ELF headers should be 64 bytes long 191 | #expect(linux_ppc64.count == 0x40) 192 | 193 | // Compare to values from readelf 194 | let header = try #require(ELF.read(linux_ppc64)) 195 | let expected = ELF( 196 | encoding: .bits64, 197 | endianness: .bigEndian, 198 | ABI: .SysV, 199 | object: .executable, 200 | ISA: .powerpc64 201 | ) 202 | #expect(header == expected) 203 | } 204 | 205 | @Test func testReadLinuxPPC64le() async throws { 206 | // Verify test data: 64-bit ELF headers should be 64 bytes long 207 | #expect(linux_ppc64le.count == 0x40) 208 | 209 | // Compare to values from readelf 210 | let header = try #require(ELF.read(linux_ppc64le)) 211 | let expected = ELF( 212 | encoding: .bits64, 213 | endianness: .littleEndian, 214 | ABI: .SysV, 215 | object: .executable, 216 | ISA: .powerpc64 217 | ) 218 | #expect(header == expected) 219 | } 220 | 221 | @Test func testReadLinuxMips64() async throws { 222 | // Verify test data: 64-bit ELF headers should be 64 bytes long 223 | #expect(linux_mips64.count == 0x40) 224 | 225 | // Compare to values from readelf 226 | let header = try #require(ELF.read(linux_mips64)) 227 | let expected = ELF( 228 | encoding: .bits64, 229 | endianness: .bigEndian, 230 | ABI: .SysV, 231 | object: .executable, 232 | ISA: .unknown(0x0008) // MIPS 233 | ) 234 | #expect(header == expected) 235 | } 236 | 237 | @Test func testReadLinuxMips64le() async throws { 238 | // Verify test data: 64-bit ELF headers should be 64 bytes long 239 | #expect(linux_mips64le.count == 0x40) 240 | 241 | // Compare to values from readelf 242 | let header = try #require(ELF.read(linux_mips64le)) 243 | let expected = ELF( 244 | encoding: .bits64, 245 | endianness: .littleEndian, 246 | ABI: .SysV, 247 | object: .executable, 248 | ISA: .unknown(0x0008) // MIPS 249 | ) 250 | #expect(header == expected) 251 | } 252 | } 253 | -------------------------------------------------------------------------------- /Tests/containertoolTests/ZlibTests.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the SwiftContainerPlugin open source project 4 | // 5 | // Copyright (c) 2024 Apple Inc. and the SwiftContainerPlugin project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of SwiftContainerPlugin project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | @testable import containertool 16 | import struct Crypto.SHA256 17 | import Testing 18 | 19 | // Check that compressing the same data on macOS and Linux produces the same output. 20 | struct ZlibTests { 21 | @Test func testGzipHeader() async throws { 22 | let data = "test" 23 | let result = gzip([UInt8](data.utf8)) 24 | #expect( 25 | "\(SHA256.hash(data: result))" 26 | == "SHA256 digest: 7dff8d09129482017247cb373e8138772e852a1a02f097d1440387055d2be69c" 27 | ) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Vendor/github.com/apple/swift-nio-extras/Sources/CNIOExtrasZlib/empty.c: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the SwiftNIO open source project 4 | // 5 | // Copyright (c) 2017-2018 Apple Inc. and the SwiftNIO project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of SwiftNIO project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | -------------------------------------------------------------------------------- /Vendor/github.com/apple/swift-nio-extras/Sources/CNIOExtrasZlib/include/CNIOExtrasZlib.h: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the SwiftNIO open source project 4 | // 5 | // Copyright (c) 2017-2018 Apple Inc. and the SwiftNIO project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of SwiftNIO project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | #ifndef C_NIO_ZLIB_H 15 | #define C_NIO_ZLIB_H 16 | 17 | #include 18 | 19 | static inline int CNIOExtrasZlib_deflateInit2(z_streamp strm, 20 | int level, 21 | int method, 22 | int windowBits, 23 | int memLevel, 24 | int strategy) { 25 | return deflateInit2(strm, level, method, windowBits, memLevel, strategy); 26 | } 27 | 28 | static inline int CNIOExtrasZlib_inflateInit2(z_streamp strm, int windowBits) { 29 | return inflateInit2(strm, windowBits); 30 | } 31 | 32 | static inline Bytef *CNIOExtrasZlib_voidPtr_to_BytefPtr(void *in) { 33 | return (Bytef *)in; 34 | } 35 | 36 | #endif 37 | -------------------------------------------------------------------------------- /Vendor/github.com/apple/swift-package-manager/Sources/Basics/AuthorizationProvider.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift open source project 4 | // 5 | // Copyright (c) 2021-2023 Apple Inc. and the Swift project authors 6 | // Licensed under Apache License v2.0 with Runtime Library Exception 7 | // 8 | // See http://swift.org/LICENSE.txt for license information 9 | // See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors 10 | // 11 | //===----------------------------------------------------------------------===// 12 | 13 | // Adapted from Sources/Basics/AuthorizationProvider.swift 14 | // Keychain and AuthorizationWriter removed. 15 | // Use of Filesystem and AbsolutePath removed. 16 | 17 | import struct Foundation.Data 18 | import struct Foundation.Date 19 | import struct Foundation.URL 20 | #if canImport(Security) 21 | import Security 22 | #endif 23 | 24 | public protocol AuthorizationProvider: Sendable { 25 | func authentication(for url: URL) -> (user: String, password: String)? 26 | } 27 | 28 | public enum AuthorizationProviderError: Error { 29 | case invalidURLHost 30 | case notFound 31 | case cannotEncodePassword 32 | case other(String) 33 | } 34 | 35 | public extension AuthorizationProvider { 36 | @Sendable 37 | func httpAuthorizationHeader(for url: URL) -> String? { 38 | guard let (user, password) = self.authentication(for: url) else { 39 | return nil 40 | } 41 | let authString = "\(user):\(password)" 42 | guard let authData = authString.data(using: .utf8) else { 43 | return nil 44 | } 45 | return "Basic \(authData.base64EncodedString())" 46 | } 47 | } 48 | 49 | // MARK: - netrc 50 | 51 | public final class NetrcAuthorizationProvider: AuthorizationProvider { 52 | let netrc: Netrc? 53 | 54 | public init(_ path: URL) throws { 55 | self.netrc = try NetrcAuthorizationProvider.load(path) 56 | } 57 | 58 | public func authentication(for url: URL) -> (user: String, password: String)? { 59 | return self.machine(for: url).map { (user: $0.login, password: $0.password) } 60 | } 61 | 62 | private func machine(for url: URL) -> Basics.Netrc.Machine? { 63 | // Since updates are appended to the end of the file, we 64 | // take the _last_ match to use the most recent entry. 65 | if let machine = NetrcAuthorizationProvider.machine(for: url), 66 | let existing = self.netrc?.machines.last(where: { $0.name.lowercased() == machine }) 67 | { 68 | return existing 69 | } 70 | 71 | // No match found. Use the first default if any. 72 | if let existing = self.netrc?.machines.first(where: { $0.isDefault }) { 73 | return existing 74 | } 75 | 76 | return .none 77 | } 78 | 79 | private static func load(_ path: URL) throws -> Netrc? { 80 | do { 81 | let content = try? String(contentsOf: path, encoding: .utf8) 82 | return try NetrcParser.parse(content ?? "") 83 | } catch NetrcError.machineNotFound { 84 | // Thrown by parse() if .netrc is empty. 85 | // SwiftPM suppresses this error, so we will follow suit. 86 | return .none 87 | } 88 | } 89 | 90 | private static func machine(for url: URL) -> String? { 91 | guard let host = url.host?.lowercased() else { 92 | return nil 93 | } 94 | return host.isEmpty ? nil : host 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /Vendor/github.com/apple/swift-package-manager/Sources/Basics/Netrc.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift open source project 4 | // 5 | // Copyright (c) 2022 Apple Inc. and the Swift project authors 6 | // Licensed under Apache License v2.0 with Runtime Library Exception 7 | // 8 | // See http://swift.org/LICENSE.txt for license information 9 | // See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors 10 | // 11 | //===----------------------------------------------------------------------===// 12 | 13 | import Foundation 14 | 15 | /// Representation of Netrc configuration 16 | public struct Netrc: Sendable { 17 | /// Representation of `machine` connection settings & `default` connection settings. 18 | /// If `default` connection settings present, they will be last element. 19 | public let machines: [Machine] 20 | 21 | fileprivate init(machines: [Machine]) { 22 | self.machines = machines 23 | } 24 | 25 | /// Returns auth information 26 | /// 27 | /// - Parameters: 28 | /// - url: The url to retrieve authorization information for. 29 | public func authorization(for url: URL) -> Authorization? { 30 | guard let index = machines.firstIndex(where: { $0.name == url.host }) ?? machines 31 | .firstIndex(where: { $0.isDefault }) 32 | else { 33 | return .none 34 | } 35 | let machine = self.machines[index] 36 | return Authorization(login: machine.login, password: machine.password) 37 | } 38 | 39 | /// Representation of connection settings 40 | public struct Machine: Equatable, Sendable { 41 | public let name: String 42 | public let login: String 43 | public let password: String 44 | 45 | public var isDefault: Bool { 46 | self.name == "default" 47 | } 48 | 49 | public init(name: String, login: String, password: String) { 50 | self.name = name 51 | self.login = login 52 | self.password = password 53 | } 54 | 55 | init?(for match: NSTextCheckingResult, string: String, variant: String = "") { 56 | guard let name = RegexUtil.Token.machine.capture(in: match, string: string) ?? RegexUtil.Token.default 57 | .capture(in: match, string: string), 58 | let login = RegexUtil.Token.login.capture(prefix: variant, in: match, string: string), 59 | let password = RegexUtil.Token.password.capture(prefix: variant, in: match, string: string) 60 | else { 61 | return nil 62 | } 63 | self = Machine(name: name, login: login, password: password) 64 | } 65 | } 66 | 67 | /// Representation of authorization information 68 | public struct Authorization: Equatable { 69 | public let login: String 70 | public let password: String 71 | 72 | public init(login: String, password: String) { 73 | self.login = login 74 | self.password = password 75 | } 76 | } 77 | } 78 | 79 | public struct NetrcParser { 80 | /// Parses stringified netrc content 81 | /// 82 | /// - Parameters: 83 | /// - content: The content to parse 84 | public static func parse(_ content: String) throws -> Netrc { 85 | let content = self.trimComments(from: content) 86 | let regex = try! NSRegularExpression(pattern: RegexUtil.netrcPattern, options: []) 87 | let matches = regex.matches( 88 | in: content, 89 | options: [], 90 | range: NSRange(content.startIndex ..< content.endIndex, in: content) 91 | ) 92 | 93 | let machines: [Netrc.Machine] = matches.compactMap { 94 | Netrc.Machine(for: $0, string: content, variant: "lp") ?? Netrc 95 | .Machine(for: $0, string: content, variant: "pl") 96 | } 97 | 98 | if let defIndex = machines.firstIndex(where: { $0.isDefault }) { 99 | guard defIndex == machines.index(before: machines.endIndex) else { 100 | throw NetrcError.invalidDefaultMachinePosition 101 | } 102 | } 103 | guard machines.count > 0 else { 104 | throw NetrcError.machineNotFound 105 | } 106 | return Netrc(machines: machines) 107 | } 108 | 109 | /// Utility method to trim comments from netrc content 110 | /// - Parameter text: String text of netrc file 111 | /// - Returns: String text of netrc file *sans* comments 112 | private static func trimComments(from text: String) -> String { 113 | let regex = try! NSRegularExpression(pattern: RegexUtil.comments, options: .anchorsMatchLines) 114 | let nsString = text as NSString 115 | let range = NSRange(location: 0, length: nsString.length) 116 | let matches = regex.matches(in: text, range: range) 117 | var trimmedCommentsText = text 118 | matches.forEach { 119 | trimmedCommentsText = trimmedCommentsText 120 | .replacingOccurrences(of: nsString.substring(with: $0.range), with: "") 121 | } 122 | return trimmedCommentsText 123 | } 124 | } 125 | 126 | public enum NetrcError: Error, Equatable { 127 | case machineNotFound 128 | case invalidDefaultMachinePosition 129 | } 130 | 131 | private enum RegexUtil { 132 | fileprivate enum Token: String, CaseIterable { 133 | case machine, login, password, account, macdef, `default` 134 | 135 | func capture(prefix: String = "", in match: NSTextCheckingResult, string: String) -> String? { 136 | guard let range = Range(match.range(withName: prefix + rawValue), in: string) else { return nil } 137 | return String(string[range]) 138 | } 139 | } 140 | 141 | static let comments: String = "\\#[\\s\\S]*?.*$" 142 | static let `default`: String = #"(?:\s*(?default))"# 143 | static let accountOptional: String = #"(?:\s*account\s+\S++)?"# 144 | static let loginPassword: String = 145 | #"\#(namedTrailingCapture("login", prefix: "lp"))\#(accountOptional)\#(namedTrailingCapture("password", prefix: "lp"))"# 146 | static let passwordLogin: String = 147 | #"\#(namedTrailingCapture("password", prefix: "pl"))\#(accountOptional)\#(namedTrailingCapture("login", prefix: "pl"))"# 148 | static let netrcPattern = 149 | #"(?:(?:(\#(namedTrailingCapture("machine"))|\#(namedMatch("default"))))(?:\#(loginPassword)|\#(passwordLogin)))"# 150 | 151 | static func namedMatch(_ string: String) -> String { 152 | #"(?:\s*(?<\#(string)>\#(string)))"# 153 | } 154 | 155 | static func namedTrailingCapture(_ string: String, prefix: String = "") -> String { 156 | #"\s*\#(string)\s+(?<\#(prefix + string)>\S++)"# 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /scripts/generate-contributors-list.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | ##===----------------------------------------------------------------------===## 3 | ## 4 | ## This source file is part of the SwiftContainerPlugin open source project 5 | ## 6 | ## Copyright (c) 2024 Apple Inc. and the SwiftContainerPlugin project authors 7 | ## Licensed under Apache License v2.0 8 | ## 9 | ## See LICENSE.txt for license information 10 | ## See CONTRIBUTORS.txt for the list of SwiftContainerPlugin project authors 11 | ## 12 | ## SPDX-License-Identifier: Apache-2.0 13 | ## 14 | ##===----------------------------------------------------------------------===## 15 | ##===----------------------------------------------------------------------===## 16 | ## 17 | ## This source file is part of the SwiftNIO open source project 18 | ## 19 | ## Copyright (c) 2017-2018 Apple Inc. and the SwiftNIO project authors 20 | ## Licensed under Apache License v2.0 21 | ## 22 | ## See LICENSE.txt for license information 23 | ## See CONTRIBUTORS.txt for the list of SwiftNIO project authors 24 | ## 25 | ## SPDX-License-Identifier: Apache-2.0 26 | ## 27 | ##===----------------------------------------------------------------------===## 28 | 29 | set -eu 30 | here="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" 31 | contributors=$( cd "$here"/.. && git shortlog -es | cut -f2 | sed 's/^/- /' ) 32 | 33 | cat > "$here/../CONTRIBUTORS.txt" <<- EOF 34 | For the purpose of tracking copyright, this is the list of individuals and 35 | organizations who have contributed source code to SwiftContainerPlugin. 36 | 37 | For employees of an organization/company where the copyright of work done 38 | by employees of that company is held by the company itself, only the company 39 | needs to be listed here. 40 | 41 | ## COPYRIGHT HOLDERS 42 | 43 | - Apple Inc. (all contributors with '@apple.com') 44 | 45 | ### Contributors 46 | 47 | $contributors 48 | 49 | **Updating this list** 50 | 51 | Please do not edit this file manually. It is generated using \`bash ./scripts/generate-contributors-list.sh\`. If a name is misspelled or appearing multiple times: add an entry in \`./.mailmap\` 52 | EOF 53 | -------------------------------------------------------------------------------- /scripts/run-integration-tests.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | ##===----------------------------------------------------------------------===## 3 | ## 4 | ## This source file is part of the SwiftContainerPlugin open source project 5 | ## 6 | ## Copyright (c) 2024 Apple Inc. and the SwiftContainerPlugin project authors 7 | ## Licensed under Apache License v2.0 8 | ## 9 | ## See LICENSE.txt for license information 10 | ## See CONTRIBUTORS.txt for the list of SwiftContainerPlugin project authors 11 | ## 12 | ## SPDX-License-Identifier: Apache-2.0 13 | ## 14 | ##===----------------------------------------------------------------------===## 15 | 16 | 17 | log() { printf -- "** %s\n" "$*" >&2; } 18 | error() { printf -- "** ERROR: %s\n" "$*" >&2; } 19 | fatal() { error "$@"; exit 1; } 20 | 21 | set -euo pipefail 22 | 23 | RUNTIME=${RUNTIME-"docker"} 24 | 25 | # Start a registry on an ephemeral port 26 | REGISTRY_ID=$($RUNTIME run -d --rm -p 127.0.0.1::5000 registry:2) 27 | export REGISTRY_HOST="localhost" 28 | REGISTRY_PORT=$($RUNTIME port "$REGISTRY_ID" 5000/tcp | sed -E 's/^.+:([[:digit:]]+)$/\1/') 29 | export REGISTRY_PORT 30 | log "Registry $REGISTRY_ID listening on $REGISTRY_HOST:$REGISTRY_PORT" 31 | 32 | # Delete the registry after running the tests, regardless of the outcome 33 | cleanup() { 34 | log "Deleting registry $REGISTRY_ID" 35 | $RUNTIME rm -f "$REGISTRY_ID" 36 | } 37 | trap cleanup EXIT 38 | 39 | log "Running smoke tests" 40 | swift test --filter 'SmokeTests' 41 | -------------------------------------------------------------------------------- /scripts/test-containertool-elf-detection.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | ##===----------------------------------------------------------------------===## 3 | ## 4 | ## This source file is part of the SwiftContainerPlugin open source project 5 | ## 6 | ## Copyright (c) 2025 Apple Inc. and the SwiftContainerPlugin project authors 7 | ## Licensed under Apache License v2.0 8 | ## 9 | ## See LICENSE.txt for license information 10 | ## See CONTRIBUTORS.txt for the list of SwiftContainerPlugin project authors 11 | ## 12 | ## SPDX-License-Identifier: Apache-2.0 13 | ## 14 | ##===----------------------------------------------------------------------===## 15 | 16 | # 17 | # This script assumes that the Static Linux SDK has already been installed 18 | # 19 | 20 | log() { printf -- "** %s\n" "$*" >&2; } 21 | error() { printf -- "** ERROR: %s\n" "$*" >&2; } 22 | fatal() { error "$@"; exit 1; } 23 | 24 | set -euo pipefail 25 | 26 | RUNTIME=${RUNTIME-"docker"} 27 | 28 | # 29 | # Create a test package 30 | # 31 | PKGPATH=$(mktemp -d) 32 | swift package --package-path "$PKGPATH" init --type executable --name hello 33 | 34 | cleanup() { 35 | log "Deleting temporary package $PKGPATH" 36 | rm -rf "$PKGPATH" 37 | } 38 | trap cleanup EXIT 39 | 40 | # 41 | # Build and package an x86_64 binary 42 | # 43 | swift build --package-path "$PKGPATH" --swift-sdk x86_64-swift-linux-musl 44 | FILETYPE=$(file "$PKGPATH/.build/x86_64-swift-linux-musl/debug/hello") 45 | log "Executable type: $FILETYPE" 46 | 47 | IMGREF=$(swift run containertool --repository localhost:5000/elf_test "$PKGPATH/.build/x86_64-swift-linux-musl/debug/hello" --from scratch) 48 | $RUNTIME pull "$IMGREF" 49 | IMGARCH=$($RUNTIME inspect "$IMGREF" --format "{{.Architecture}}") 50 | if [ "$IMGARCH" = "amd64" ] ; then 51 | log "x86_64 detection: PASSED" 52 | else 53 | fatal "x86_64 detection: FAILED - image architecture was $IMGARCH; expected amd64" 54 | fi 55 | 56 | # 57 | # Build and package an aarch64 binary 58 | # 59 | swift build --package-path "$PKGPATH" --swift-sdk aarch64-swift-linux-musl 60 | FILETYPE=$(file "$PKGPATH/.build/x86_64-swift-linux-musl/debug/hello") 61 | log "Executable type: $FILETYPE" 62 | 63 | IMGREF=$(swift run containertool --repository localhost:5000/elf_test "$PKGPATH/.build/aarch64-swift-linux-musl/debug/hello" --from scratch) 64 | $RUNTIME pull "$IMGREF" 65 | IMGARCH=$($RUNTIME inspect "$IMGREF" --format "{{.Architecture}}") 66 | if [ "$IMGARCH" = "arm64" ] ; then 67 | log "aarch64 detection: PASSED" 68 | else 69 | fatal "aarch64 detection: FAILED - image architecture was $IMGARCH; expected arm64" 70 | fi 71 | -------------------------------------------------------------------------------- /scripts/test-containertool-resources.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | ##===----------------------------------------------------------------------===## 3 | ## 4 | ## This source file is part of the SwiftContainerPlugin open source project 5 | ## 6 | ## Copyright (c) 2025 Apple Inc. and the SwiftContainerPlugin project authors 7 | ## Licensed under Apache License v2.0 8 | ## 9 | ## See LICENSE.txt for license information 10 | ## See CONTRIBUTORS.txt for the list of SwiftContainerPlugin project authors 11 | ## 12 | ## SPDX-License-Identifier: Apache-2.0 13 | ## 14 | ##===----------------------------------------------------------------------===## 15 | 16 | log() { printf -- "** %s\n" "$*" >&2; } 17 | error() { printf -- "** ERROR: %s\n" "$*" >&2; } 18 | fatal() { error "$@"; exit 1; } 19 | 20 | set -euo pipefail 21 | 22 | RUNTIME=${RUNTIME-"docker"} 23 | PKGPATH=$(mktemp -d) 24 | 25 | # 26 | # Package an example payload with resources. This test only checks 27 | # that the correct files are in the container image and does not run 28 | # it, so the payload does not need to be an executable. 29 | # 30 | touch "$PKGPATH/hello" 31 | mkdir -p "$PKGPATH/resourcedir" 32 | touch "$PKGPATH/resourcedir/resource1.txt" "$PKGPATH/resourcedir/resource2.txt" "$PKGPATH/resourcedir/resource3.txt" 33 | touch "$PKGPATH/resourcefile.dat" 34 | swift run containertool \ 35 | "$PKGPATH/hello" \ 36 | --repository localhost:5000/resource_test \ 37 | --from scratch \ 38 | --resources "$PKGPATH/resourcedir" \ 39 | --resources "$PKGPATH/resourcefile.dat" 40 | 41 | $RUNTIME rm -f resource-test 42 | $RUNTIME create --pull always --name resource-test localhost:5000/resource_test:latest 43 | 44 | cleanup() { 45 | log "Deleting temporary package $PKGPATH" 46 | rm -rf "$PKGPATH" 47 | 48 | log "Deleting resource-test container" 49 | $RUNTIME rm -f resource-test 50 | } 51 | trap cleanup EXIT 52 | 53 | 54 | for resource in \ 55 | /hello \ 56 | /resourcedir/resource1.txt \ 57 | /resourcedir/resource2.txt \ 58 | /resourcedir/resource3.txt \ 59 | /resourcefile.dat 60 | do 61 | # This will return a non-zero exit code if the file does not exist 62 | if $RUNTIME cp resource-test:$resource - > /dev/null ; then 63 | log "$resource: OK" 64 | else 65 | fatal "$resource: FAILED" 66 | fi 67 | done 68 | 69 | -------------------------------------------------------------------------------- /scripts/test-plugin-output-streaming.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | ##===----------------------------------------------------------------------===## 3 | ## 4 | ## This source file is part of the SwiftContainerPlugin open source project 5 | ## 6 | ## Copyright (c) 2025 Apple Inc. and the SwiftContainerPlugin project authors 7 | ## Licensed under Apache License v2.0 8 | ## 9 | ## See LICENSE.txt for license information 10 | ## See CONTRIBUTORS.txt for the list of SwiftContainerPlugin project authors 11 | ## 12 | ## SPDX-License-Identifier: Apache-2.0 13 | ## 14 | ##===----------------------------------------------------------------------===## 15 | 16 | # Test that error output streamed from containertool is printed correctly by the plugin. 17 | 18 | set -euo pipefail 19 | 20 | log() { printf -- "** %s\n" "$*" >&2; } 21 | error() { printf -- "** ERROR: %s\n" "$*" >&2; } 22 | fatal() { error "$@"; exit 1; } 23 | 24 | # Work in a temporary directory, deleted after the test finishes 25 | PKGPATH=$(mktemp -d) 26 | cleanup() { 27 | log "Deleting temporary package $PKGPATH" 28 | rm -rf "$PKGPATH" 29 | } 30 | trap cleanup EXIT 31 | 32 | # Create a test project which depends on this checkout of the plugin repository 33 | REPO_ROOT=$(git rev-parse --show-toplevel) 34 | swift package --package-path "$PKGPATH" init --type executable --name hello 35 | cat >> "$PKGPATH/Package.swift" <&1 | tee "$PKGPATH/output" 46 | set -o pipefail 47 | 48 | # This checks that the output lines are not broken, but not that they appear in the correct order 49 | grep -F -x -e "error: Please specify the destination repository using --repository or CONTAINERTOOL_REPOSITORY" \ 50 | -e "error: Usage: containertool [] " \ 51 | -e "error: See 'containertool --help' for more information." "$PKGPATH/output" 52 | 53 | log Plugin error output: PASSED 54 | 55 | --------------------------------------------------------------------------------