├── .gitignore
├── .dockerignore
├── ubuntu.Dockerfile
├── swift.Dockerfile
├── Package.swift
├── Sources
├── BuildAndTagScript
│ ├── Util.swift
│ ├── GenerateHubREADME.swift
│ ├── BuildAndTagCommand.swift
│ └── ImageSpecifications.swift
└── buildAndTag
│ └── main.swift
├── README.md
├── LICENSE
├── templates
├── vapor3.leaf
└── vapor.leaf
└── Package.resolved
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | .build
3 | .swiftpm
4 |
5 |
--------------------------------------------------------------------------------
/.dockerignore:
--------------------------------------------------------------------------------
1 | .build
2 | .swiftpm
3 | Package.*
4 | README.md
5 | Sources/
6 |
7 |
--------------------------------------------------------------------------------
/ubuntu.Dockerfile:
--------------------------------------------------------------------------------
1 | ARG UBUNTU_OS_IMAGE_VERSION
2 | FROM ubuntu:${UBUNTU_OS_IMAGE_VERSION}
3 |
4 | ARG UBUNTU_VERSION_SPECIFIC_APT_DEPENDENCIES
5 | # DEBIAN_FRONTEND=noninteractive for automatic UTC configuration in tzdata
6 | RUN apt-get -qq update && DEBIAN_FRONTEND=noninteractive apt-get install -y \
7 | libatomic1 libxml2 libz-dev libbsd0 tzdata ${UBUNTU_VERSION_SPECIFIC_APT_DEPENDENCIES} \
8 | && rm -r /var/lib/apt/lists/*
9 |
--------------------------------------------------------------------------------
/swift.Dockerfile:
--------------------------------------------------------------------------------
1 | ARG SWIFT_BASE_IMAGE
2 |
3 | FROM ${SWIFT_BASE_IMAGE}
4 |
5 | RUN export DEBIAN_FRONTEND=noninteractive DEBCONF_NONINTERACTIVE_SEEN=true && apt-get -q update
6 |
7 | ARG ADDITIONAL_APT_DEPENDENCIES=
8 |
9 | RUN export DEBIAN_FRONTEND=noninteractive DEBCONF_NONINTERACTIVE_SEEN=true && \
10 | apt-get -q install -y \
11 | zlib1g-dev \
12 | ${ADDITIONAL_APT_DEPENDENCIES} \
13 | && rm -r /var/lib/apt/lists/*
14 |
15 | RUN swift --version
16 |
--------------------------------------------------------------------------------
/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version:5.2
2 | import PackageDescription
3 |
4 | let package = Package(
5 | name: "docker",
6 | platforms: [.macOS(.v10_15)],
7 | dependencies: [
8 | .package(url: "https://github.com/vapor/console-kit.git", from: "4.0.0"),
9 | .package(url: "https://github.com/vapor/leaf-kit.git", from: "1.0.0-rc"),
10 | ],
11 | targets: [
12 | .target(name: "BuildAndTagScript", dependencies: [
13 | .product(name: "ConsoleKit", package: "console-kit"),
14 | .product(name: "LeafKit", package: "leaf-kit"),
15 | ]),
16 | .target(name: "buildAndTag", dependencies: ["BuildAndTagScript"]),
17 | ]
18 | )
19 |
--------------------------------------------------------------------------------
/Sources/BuildAndTagScript/Util.swift:
--------------------------------------------------------------------------------
1 | import ConsoleKit
2 |
3 | func list(specs: [ImageSpecification], heading: String, in context: CommandContext, verbose: Bool, debug: Bool) {
4 | context.console.info(heading)
5 | for spec in specs {
6 | context.console.info(" - \(spec.tag)", newLine: !(verbose || debug))
7 | if verbose || debug {
8 | context.console.info(" [\(spec.buildOrder)] \(spec.buildArguments.map { "\($0)=\($1)" }.joined(separator: ", "))")
9 | if debug {
10 | context.console.info("\tCTX:[\(spec.autoGenerationContext.map { "\($0)=\($1)" })]")
11 | }
12 | }
13 | }
14 | context.console.info()
15 | }
16 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 | **This repository is now deprecated in favour of the standard Swift Docker images**
23 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2020 Qutheory, LLC
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/templates/vapor3.leaf:
--------------------------------------------------------------------------------
1 | \#\# Supported tags
2 |
3 | #for(line in specsByVersion):
4 | - #for(spec in line):`#(spec.tag)`#if(!isLast):, #endif #endfor
5 | #endfor
6 |
7 | \#\# Quick reference
8 |
9 | - [Vapor Homepage](https://vapor.codes)
10 |
11 | - [Vapor Docs](https://docs.vapor.codes/4.0/)
12 |
13 | - [Vapor 4-compatible modern images](https://hub.docker.com/r/vapor/swift)
14 |
15 | - [Source of these images](https://github.com/vapor/docker.git)
16 |
17 | \#\# How to use these images
18 |
19 | Use them where you would otherwise use a `swift` image of the same version to avoid having to manually keep track of Vapor's dependencies and spend time building them each time you use Docker. These images are specifically intended for use with legacy Vapor 3 projects.
20 |
21 | \#\# Image Variants
22 |
23 | \#\#\# `vapor3/swift:`
24 |
25 | The image built from the corresponding `swift:` image, containing Vapor's compile-time dependencies preinstalled.
26 |
27 | \#\#\# `vapor3/swift:-ci`
28 |
29 | A version of the base Vapor image which includes the `curl` APT package preinstalled. Intended for use by CI systems that expect to find it available.
30 |
31 | \# License
32 |
33 | [MIT license.](https://github.com/vapor/docker/blob/master/LICENSE)
34 |
--------------------------------------------------------------------------------
/Package.resolved:
--------------------------------------------------------------------------------
1 | {
2 | "object": {
3 | "pins": [
4 | {
5 | "package": "console-kit",
6 | "repositoryURL": "https://github.com/vapor/console-kit.git",
7 | "state": {
8 | "branch": null,
9 | "revision": "7a97a5ea7fefe61cf2c943242113125b0f396a98",
10 | "version": "4.1.0"
11 | }
12 | },
13 | {
14 | "package": "leaf-kit",
15 | "repositoryURL": "https://github.com/vapor/leaf-kit.git",
16 | "state": {
17 | "branch": null,
18 | "revision": "b6c8edd71a5387c4b48ecafe5be4cadd4f896193",
19 | "version": "1.0.0-rc.1.4"
20 | }
21 | },
22 | {
23 | "package": "swift-log",
24 | "repositoryURL": "https://github.com/apple/swift-log.git",
25 | "state": {
26 | "branch": null,
27 | "revision": "74d7b91ceebc85daf387ebb206003f78813f71aa",
28 | "version": "1.2.0"
29 | }
30 | },
31 | {
32 | "package": "swift-nio",
33 | "repositoryURL": "https://github.com/apple/swift-nio.git",
34 | "state": {
35 | "branch": null,
36 | "revision": "e876fb37410e0036b98b5361bb18e6854739572b",
37 | "version": "2.16.0"
38 | }
39 | }
40 | ]
41 | },
42 | "version": 1
43 | }
44 |
--------------------------------------------------------------------------------
/Sources/buildAndTag/main.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import ConsoleKit
3 | import BuildAndTagScript
4 |
5 | #if Xcode
6 | do {
7 | if let srcdir = ((try? PropertyListSerialization.propertyList(from:
8 | Data(contentsOf: URL(fileURLWithPath: FileManager.default.currentDirectoryPath
9 | .appending("/../../../info.plist"))), options: [], format: nil)) as? [String: Any])?["WorkspacePath"] as? String,
10 | FileManager.default.fileExists(atPath: "\(srcdir)/Package.swift")
11 | {
12 | FileManager.default.changeCurrentDirectoryPath(srcdir)
13 | } else if let srcdir = URL?(URL(fileURLWithPath: #file).deletingLastPathComponent().deletingLastPathComponent().deletingLastPathComponent()),
14 | let _ = try? srcdir.appendingPathComponent("Package.swift").checkResourceIsReachable() {
15 | FileManager.default.changeCurrentDirectoryPath(srcdir.path)
16 | }
17 | }
18 | #endif
19 |
20 | let console: Console = Terminal()
21 | var input = CommandInput(arguments: CommandLine.arguments)
22 | var context = CommandContext(console: console, input: input)
23 |
24 | var commands = Commands(enableAutocomplete: true)
25 | commands.use(BuildAndTagCommand(), as: "build", isDefault: true)
26 | commands.use(GenerateHubREADMECommand(), as: "readmes", isDefault: true)
27 |
28 | do {
29 | let group = commands
30 | .group(help: "Build and tag script")
31 | try console.run(group, input: input)
32 | } catch let error {
33 | console.error("\(error)")
34 | exit(1)
35 | }
36 |
--------------------------------------------------------------------------------
/templates/vapor.leaf:
--------------------------------------------------------------------------------
1 | 
2 |
3 | \# Vapor 4
4 |
5 | \#\# Supported tags
6 |
7 | #for(line in specsByVersion):
8 | - #for(spec in line):`#(spec.tag)`#if(!isLast):, #endif #endfor
9 | #endfor
10 |
11 | \#\# Quick reference
12 |
13 | - [Vapor Homepage](https://vapor.codes)
14 |
15 | - [Vapor Docs](https://docs.vapor.codes/4.0/)
16 |
17 | - [Vapor 3-compatible legacy images](https://hub.docker.com/r/vapor3/swift)
18 |
19 | - [Source of these images](https://github.com/vapor/docker.git)
20 |
21 | \#\# How to use these images
22 |
23 | Use these where you would otherwise use a `swift` image of the same version to avoid having to manually keep track of Vapor's dependencies and spend time building them each time you use Docker.
24 |
25 | \#\# Image Variants
26 |
27 | \#\#\# `vapor/swift:`
28 |
29 | The image built from the corresponding `swift:` image, containing Vapor's compile-time dependencies preinstalled.
30 |
31 | \#\#\# `vapor/swift:-ci`
32 |
33 | A version of the base Vapor image which includes the `curl` APT package preinstalled. Intended for use by CI systems that expect to find it available.
34 |
35 | \#\#\# `vapor/swift:master`
36 |
37 | A version of the Vapor base image built from the latest `swiftlang/swift:nightly-master` image at the time of the last upload of Vapor images. Updated frequently but, at this time, not automatically.
38 |
39 | \#\#\# `vapor/ubuntu:`
40 |
41 | The image built from a corresponding `ubuntu:` image, containing Vapor's _runtime_ dependencies preinstalled. Most useful when building a Docker image for a Vapor app in two stages. Use a `vapor/swift` image as the builder, and a `vapor/ubuntu` image as the runner.
42 |
43 | \# License
44 |
45 | [MIT license.](https://github.com/vapor/docker/blob/master/LICENSE)
46 |
--------------------------------------------------------------------------------
/Sources/BuildAndTagScript/GenerateHubREADME.swift:
--------------------------------------------------------------------------------
1 | import ConsoleKit
2 | import LeafKit
3 | import Foundation
4 |
5 | struct LeafEmbeddedFiles: LeafFiles { // think "EmbbeddedRunLoop"
6 | func file(path: String, on eventLoop: EventLoop) -> EventLoopFuture {
7 | do {
8 | let data = try Data(contentsOf: URL(fileURLWithPath: path, isDirectory: false), options: .mappedIfSafe)
9 | var buffer = ByteBufferAllocator().buffer(capacity: data.count)
10 | buffer.writeBytes(data)
11 | return eventLoop.makeSucceededFuture(buffer)
12 | } catch {
13 | return eventLoop.makeFailedFuture(error)
14 | }
15 | }
16 | }
17 |
18 | public final class GenerateHubREADMECommand: Command {
19 | public var help = "Generate README files for each repository based on Leaf templates"
20 |
21 | public struct Signature: CommandSignature {
22 | @Flag(name: "verbose", short: "v", help: "Print additional information about progress to the console,")
23 | var verbose: Bool
24 |
25 | @Flag(name: "debug", short: "D", help: "Enable additional debugging information output. Implies -v.")
26 | var debug: Bool
27 |
28 | public init() {}
29 | }
30 |
31 | public init() {}
32 |
33 | public func run(using context: CommandContext, signature: Signature) throws {
34 | let elg = MultiThreadedEventLoopGroup(numberOfThreads: 1)
35 | defer { try? elg.syncShutdownGracefully() }
36 |
37 | let renderer = LeafRenderer(
38 | configuration: .init(rootDirectory: Array(URL(fileURLWithPath: #file, isDirectory: false).pathComponents.dropLast(3)).joined(separator: "/")),
39 | files: LeafEmbeddedFiles(),
40 | eventLoop: elg.next())
41 |
42 | let orgs = Dictionary(grouping: getAllPreconfiguredSpecs(), by: { $0.tag.prefix { $0 != "/" } })
43 |
44 | for (org, specs) in orgs {
45 | guard !specs.isEmpty else { continue }
46 | list(specs: specs, heading: "Found \(specs.count) specs in \(org) org:", in: context, verbose: signature.verbose, debug: signature.debug)
47 |
48 | var leafData: [String: LeafData] = [:]
49 |
50 | let splitSpecs: [[ImageSpecification]] = specs.reduce(into: [[specs[0]]]) { a, s in
51 | s.tag.hasPrefix(a.last![0].tag.prefix { $0 != "-" }) ? a[a.endIndex - 1].append(s) : a.append([s]) }
52 |
53 | leafData["org"] = .string(String(org))
54 | leafData["specsByVersion"] = .array(splitSpecs.map { .array($0.map { ["tag": .string($0.tag)] }) })
55 |
56 | let rendered = try renderer.render(path: "templates/\(org).leaf", context: leafData).wait()
57 | let renderedBlob = rendered.readableBytesView
58 |
59 | context.console.info("📄 Generated README for \(org) org!")
60 | if signature.debug {
61 | context.console.info("Dumping generated contents...")
62 | context.console.info()
63 | String(bytes: renderedBlob, encoding: .utf8)!.components(separatedBy: "\n").forEach {
64 | context.console.print($0)
65 | }
66 | }
67 | context.console.info()
68 |
69 | context.console.info("📄 Saving to \(FileManager.default.currentDirectoryPath.appending("/\(org).md"))")
70 | try Data(renderedBlob).write(to: URL(fileURLWithPath: "", isDirectory: true).appendingPathComponent("\(org).md"))
71 | context.console.info("📄 Done!")
72 | context.console.info()
73 | }
74 |
75 | context.console.info("All READMEs generated!")
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/Sources/BuildAndTagScript/BuildAndTagCommand.swift:
--------------------------------------------------------------------------------
1 | import ConsoleKit
2 | import Foundation
3 |
4 | public enum ColorOutputSetting: RawRepresentable, Equatable, Hashable, LosslessStringConvertible, CaseIterable, ExpressibleByNilLiteral, ExpressibleByBooleanLiteral {
5 | case automatic // use the default
6 | case forcedOn // force color output on
7 | case forcedOff // force color output off
8 |
9 | public init?(rawValue: String?) { switch rawValue?.lowercased() {
10 | case "yes", "on", "true":
11 | self = .forcedOn
12 | case "no", "off", "false":
13 | self = .forcedOff
14 | case "auto", .none:
15 | self = .automatic
16 | default:
17 | return nil
18 | } }
19 |
20 | public init?(_ description: String) { self.init(rawValue: description) }
21 |
22 | public init(nilLiteral: ()) { self = .automatic }
23 |
24 | public init(booleanLiteral value: BooleanLiteralType) { self = value ? .forcedOn : .forcedOff }
25 |
26 | public var rawValue: String? { switch self {
27 | case .automatic: return "auto"
28 | case .forcedOff: return "off"
29 | case .forcedOn: return "on"
30 | } }
31 |
32 | public var description: String { self.rawValue! }
33 |
34 | public var sytlizedOutputOverrideValue: Bool? { switch self {
35 | case .automatic: return nil
36 | case .forcedOff: return false
37 | case .forcedOn: return true
38 | } }
39 | }
40 |
41 | extension Console {
42 | func dockerBuildCustomActivity() -> ActivityIndicator {
43 | return self.customActivity(
44 | frames: [
45 | "[💨 📦 ]",
46 | "[💨🏷 📦 ]",
47 | "[💨 🏷 📦 ]",
48 | "[💨 🏷 📦 ]",
49 | "[💨 🏷 📦 ]",
50 | "[💨 🏷 📦 ]",
51 | "[💨 🏷 📦 ]",
52 | "[💨 🏷 📦 ]",
53 | "[💨 🏷 📦 ]",
54 | "[💨 🏷 📦 ]",
55 | "[💨 🏷 📦 ]",
56 | "[💨 🏷 📦 ]",
57 | "[💨 🏷 📦 ]",
58 | "[💨 🏷 📦 ]",
59 | "[💨 🏷 📦 ]",
60 | "[💨 🏷 📦 ]",
61 | "[💨 🏷 📦 ]",
62 | "[💨 🏷 📦 ]",
63 | "[💨 🏷📦 ]",
64 | "[💨 🎁 ]",
65 | "[💨 🎁 ]",
66 | "[💨 🎁 ]",
67 | "[💨 🎁 ]",
68 | ],
69 | success: "[ 🎁✅]",
70 | failure: "[ 🎁❌]"
71 | )
72 | }
73 | }
74 |
75 | public final class BuildAndTagCommand: Command {
76 | public struct Signature: CommandSignature {
77 | @Option(name: "color", help: "Enables colorized output (yes|no|auto)")
78 | var color: ColorOutputSetting?
79 |
80 | @Flag(name: "stop-after-build", short: "d" /* as in dry run */, help: "Don't push images, just build them")
81 | var stopAfterBuild: Bool
82 |
83 | @Flag(name: "dry-run-for-push", short: "N", help: "Generate and show the commands to push images without actually pushing them")
84 | var dontReallyPushImages: Bool
85 |
86 | @Option(name: "skip-repos", help: "Comma-separated list of image respositories to skip.")
87 | var excludedRepos: String?
88 |
89 | @Option(name: "skip-versions", help: "Comma-separated list of Swift versions to skip.")
90 | var excludedSwiftVersions: String?
91 |
92 | @Option(name: "skip-images", help: "Comma-separated list of fully-qualified image tags to skip.")
93 | var excludedImageTags: String?
94 |
95 | @Flag(name: "headless", short: "y", help: "Don't prompt for confirmation of actions")
96 | var headlessMode: Bool
97 |
98 | @Flag(name: "verbose", short: "v", help: "Run `docker build` and `docker push` in verbose mode")
99 | var verbose: Bool
100 |
101 | @Flag(name: "debug", short: "D", help: "Enable printing extra debugging info. Implies -v.")
102 | var debug: Bool
103 |
104 | public init() {}
105 | }
106 |
107 | public init() {
108 | interruptSource.setEventHandler { [weak self] in
109 | if self?.currentTask?.isRunning ?? false {
110 | self?.currentTask?.interrupt()
111 | }
112 | signal(SIGINT, SIG_DFL)
113 | raise(SIGINT)
114 | }
115 | interruptSource.resume()
116 | signal(SIGINT, SIG_IGN)
117 | }
118 |
119 | deinit {
120 | interruptSource.cancel()
121 | }
122 |
123 | public var help: String { "Builds, tags, and pushes prebuilt Docker images" }
124 |
125 | private var interruptSource = DispatchSource.makeSignalSource(signal: SIGINT, queue: DispatchQueue.main)
126 | private var currentTask: Process? = nil
127 | func runCommand(_ command: [String], preservingStreams: Bool) throws -> Int32 {
128 | precondition(currentTask == nil)
129 | currentTask = Process()
130 | defer { currentTask = nil }
131 | currentTask?.executableURL = URL(fileURLWithPath: "/usr/bin/env", isDirectory: false)
132 | currentTask?.arguments = command
133 | currentTask?.standardInput = preservingStreams ? FileHandle.standardInput : FileHandle.nullDevice
134 | currentTask?.standardOutput = preservingStreams ? FileHandle.standardOutput : FileHandle.nullDevice
135 | currentTask?.standardError = preservingStreams ? FileHandle.standardError : FileHandle.nullDevice
136 | try currentTask?.run()
137 | currentTask?.waitUntilExit()
138 | return currentTask!.terminationStatus
139 | }
140 |
141 | func getAndPrintSpecs(context: CommandContext, signature: Signature) -> [ImageSpecification] {
142 | let excludedRepos = signature.excludedRepos?.components(separatedBy: ",") ?? []
143 | let excludedSwiftVersions = signature.excludedSwiftVersions?.components(separatedBy: ",") ?? []
144 | let excludedTags = signature.excludedImageTags?.components(separatedBy: ",") ?? []
145 | let specs = getAllPreconfiguredSpecs(excludingRepositories: excludedRepos, swiftVersions: excludedSwiftVersions, tags: excludedTags)
146 |
147 | list(specs: specs, heading: "Building \(specs.count) images:", in: context, verbose: signature.verbose, debug: signature.debug)
148 | return specs
149 | }
150 |
151 | public func run(using context: CommandContext, signature: Signature) throws {
152 | context.console.stylizedOutputOverride = (signature.color ?? .automatic).sytlizedOutputOverrideValue
153 |
154 | let specs = getAndPrintSpecs(context: context, signature: signature)
155 |
156 | if !signature.stopAfterBuild {
157 | context.console.warning("Built images will be pushed to Docker Hub.")
158 | context.console.warning()
159 | }
160 |
161 | if !signature.headlessMode {
162 | guard context.console.confirm("Proceed (y/n)?") else { return }
163 | }
164 |
165 | for spec in specs {
166 | context.console.output("📦 Building \(spec.tag)", style: .init(color: .brightCyan))
167 |
168 | var buildCommand = ["docker", "build", "--pull", "--tag", spec.tag, "--label", "codes.vapor.images.prebuilt=1", "--file", spec.dockerfile]
169 |
170 | if !signature.verbose || signature.debug {
171 | buildCommand.append("--quiet")
172 | }
173 | buildCommand.append(contentsOf: spec.buildArguments.flatMap { ["--build-arg", "\($0.key)=\($0.value)"] })
174 | buildCommand.append(contentsOf: spec.extraBuildOptions)
175 | buildCommand.append(".")
176 |
177 | var indicator: ActivityIndicator? = nil
178 |
179 | if signature.verbose || signature.debug {
180 | context.console.output("Running build command: \(buildCommand.joined(separator: " "))", style: .init(color: .brightGreen))
181 | } else {
182 | indicator = context.console.dockerBuildCustomActivity()
183 | }
184 | indicator?.start(refreshRate: 50)
185 | if try runCommand(buildCommand, preservingStreams: signature.verbose || signature.debug) == 0 {
186 | if signature.verbose || signature.debug {
187 | context.console.output("Finished building image \(spec.tag)", style: .init(color: .brightGreen))
188 | }
189 | indicator?.succeed()
190 | context.console.output("")
191 | } else {
192 | indicator?.fail()
193 | context.console.error("Build command failed! Stopping here.")
194 | return
195 | }
196 | }
197 |
198 | context.console.info("📦 All images built! 📦")
199 | guard !signature.stopAfterBuild else { return }
200 |
201 | for spec in specs {
202 | context.console.output("📦📤 Pushing \(spec.tag)...", style: .init(color: .brightCyan))
203 |
204 | let pushCommand = ["docker", "push", spec.tag]
205 |
206 | if signature.verbose || signature.debug || signature.dontReallyPushImages {
207 | context.console.output("📦📤 Running push command: \(pushCommand.joined(separator: " "))", style: .init(color: .brightGreen))
208 | }
209 | if signature.dontReallyPushImages {
210 | context.console.output("📦📤 As requested, not actually running push command...", style: .init(color: .blue))
211 | } else if try runCommand(pushCommand, preservingStreams: true) == 0 {
212 | context.console.output("📦📤 \(spec.tag) is pushed!", style: .init(color: .brightCyan))
213 | } else {
214 | context.console.error("Push command failed! Stopping here.")
215 | return
216 | }
217 | }
218 |
219 | if !signature.dontReallyPushImages {
220 | context.console.output("🎉📦💧 Successfully built and pushed \(specs.count) images! 💧📦🎉", style: .init(color: .brightWhite, isBold: true))
221 | }
222 | }
223 | }
224 |
--------------------------------------------------------------------------------
/Sources/BuildAndTagScript/ImageSpecifications.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | // MARK: - Perfunctory data models
4 |
5 | public struct ImageBuilderConfiguration: Hashable, Equatable { // container for global options and a list of repos
6 | public let repositories: [ImageRepository]
7 | public let replacements: [String: String]
8 |
9 | public init(globalReplacements: [String: String] = [:], _ repositories: ImageRepository...) {
10 | self.init(globalReplacements: globalReplacements, repositories)
11 | }
12 |
13 | public init(globalReplacements: [String: String] = [:], _ repositories: [ImageRepository]) {
14 | self.repositories = repositories
15 | self.replacements = globalReplacements
16 | }
17 | }
18 |
19 | public struct ImageRepository: Hashable, Equatable {
20 | public let name: String // org + repo as appears in a tag like $ORG/$REPO:$VERSION-$VARIANT
21 |
22 | public let defaultDockerfile: String // relative from root, overrides default in overall specs
23 | public var replacements: [String : String]
24 | public var template: ImageTemplate // a template used for auto-generating a set of variants
25 | public let generatingVariations: ImageAutoVariantGroup // a set of variations to be automatically permuted and built
26 |
27 | public init(name: String, defaultDockerfile: String, replacements: [String: String] = [:], template: ImageTemplate, _ generatingVariations: ImageAutoVariantKeyedSet...) { // do a bunch of silly rigamarole to make sure the org and repo names are saved off where they're wanted
28 | self.name = name
29 | self.defaultDockerfile = defaultDockerfile
30 | self.replacements = replacements
31 | self.template = template
32 | self.generatingVariations = ImageAutoVariantGroup(sets: generatingVariations)
33 | }
34 |
35 | func permutedVariants(withGlobalReplacements globalReplacements: [String: String]) -> [[String: String]] {
36 | return self.generatingVariations.permute(withBaseReplacements: globalReplacements.updating(with: self.replacements))
37 | }
38 |
39 | func makeImageSpecs(withGlobalReplacements globalReplacements: [String: String]) -> [ImageSpecification] {
40 | func doReplacements(in template: String, using replacements: [String: String]) -> String {
41 | var result = template
42 | while let match = result.range(of: "\\$\\{[A-Za-z_:]+?\\}", options: .regularExpression) {
43 | if result[match] == "${:trimStems}" {
44 | result = result.replacingCharacters(in: match, with: "").replacingOccurrences(of: "-{2,}", with: "-", options: .regularExpression).replacingOccurrences(of: "-$", with: "", options: .regularExpression)
45 | } else if result[match] == "${:trim}" {
46 | result = result.replacingCharacters(in: match, with: "").trimmingCharacters(in: .whitespacesAndNewlines)
47 | } else if result[match] == "${:chopVersion}" {
48 | result = result.replacingCharacters(in: result.index(match.lowerBound, offsetBy: -2).. [[String: String]] {
83 | self.sets.reduce([replacements]) { p, set in set.valuesMergingKey.flatMap { v in p.map { $0.updating(with: v) } } }
84 | }
85 | }
86 |
87 | public struct ImageAutoVariantKeyedSet: Hashable, Equatable {
88 | let key: String
89 | let values: [ImageAutoVariantSetValue]
90 |
91 | var valuesMergingKey: [[String: String]] {
92 | self.values.map { $0.asRawValue }.map { v in [self.key: v.name].updating(with: v.replacements) }
93 | }
94 |
95 | init(_ key: String, _ values: ImageAutoVariantSetValue...) {
96 | self.key = key
97 | self.values = values
98 | }
99 | }
100 |
101 | public enum ImageAutoVariantSetValue: Hashable, Equatable {
102 | case empty // the empty string value; a variant to represent "normal"
103 | case value(String) // simple value; a variant where the set key plus this name is enough to fully specify
104 | case valueAndKeys(String, [String: String]) // a variant giving its name plus additional replacements needed to specify it
105 |
106 | var asRawValue: (name: String, replacements: [String: String]) { switch self {
107 | case .empty: return (name: "", replacements: [:])
108 | case .value(let name): return (name: name, replacements: [:])
109 | case .valueAndKeys(let name, let replacements): return (name: name, replacements: replacements)
110 | } }
111 | }
112 |
113 | public struct ImageSpecification: Codable, Hashable, Equatable { // self-contained specification of everything required to build a single image
114 | public let tag: String // verbatim tag passed to `docker build -t`
115 | public let dockerfile: String // relative from root
116 | public let buildArguments: [String: String] // values for `--build-arg`, fully evaluated for any replacements
117 | public let extraBuildOptions: [String] // additional commands options for `docker build`. avoid if possible.
118 | public let buildOrder: Int // the original index this spec had in the permutation matrix for the repo that it belongs to
119 | public let autoGenerationContext: [String: String] // contains the final set of replacements that were applied to the template after permutation and cascading
120 | }
121 |
122 | // MARK: - Logic to get the set of desired specs in usable form
123 |
124 | public func getAllPreconfiguredSpecs(
125 | excludingRepositories excludedRepos: [String] = [],
126 | swiftVersions excludedSwiftVersions: [String] = [],
127 | tags excludedTags: [String] = []
128 | ) -> [ImageSpecification] {
129 | var specs: [ImageSpecification] = []
130 |
131 | for repo in ImageBuilderConfiguration.preconfiguredImageSpecifications.repositories {
132 | guard !excludedRepos.contains(repo.name) else { continue }
133 | specs.append(contentsOf: repo.makeImageSpecs(withGlobalReplacements: ImageBuilderConfiguration.preconfiguredImageSpecifications.replacements))
134 | }
135 | specs.removeAll { spec in excludedTags.contains(spec.tag) || excludedSwiftVersions.contains { spec.tag.contains("swift:\($0)") } }
136 | return specs
137 | }
138 |
139 | // MARK: - Very helpful utility method
140 |
141 | extension Dictionary {
142 | public func updating(with other: [Key : Value]) -> [Key : Value] { self.merging(other, uniquingKeysWith: { $1 }) }
143 | }
144 |
145 | // MARK: - Actual specs!
146 |
147 | extension ImageBuilderConfiguration {
148 |
149 | private static var commonSwiftImageTemplate: ImageTemplate {
150 | .init(
151 | nameTemplate: "${REPOSITORY_NAME}:${SWIFT_VERSION}-${IMAGE_OS_VERSION}-${IMAGE_VAPOR_VARIANT}${:trimStems}",
152 | buildArguments: [
153 | "SWIFT_BASE_IMAGE": "${SWIFT_BASE_REPO_NAME}:${SWIFT_BASE_VERSION}-${IMAGE_OS_VERSION}${:trimStems}",
154 | "ADDITIONAL_APT_DEPENDENCIES": "${LIBSSL_DEPENDENCY} ${CURL_DEPENDENCY}${:trim}"
155 | ]
156 | )
157 | }
158 |
159 | private static var ubuntuXenialDeps: [String: String] { ["UBUNTU_VERSION_SPECIFIC_APT_DEPENDENCIES": "libicu55 libcurl3"] }
160 | private static var ubuntuBionicDeps: [String: String] { ["UBUNTU_VERSION_SPECIFIC_APT_DEPENDENCIES": "libicu60 libcurl4"] }
161 |
162 | public static var preconfiguredImageSpecifications: Self { return .init(
163 |
164 | globalReplacements: [
165 | "SWIFT_LATEST_RELEASE_VERSION": "5.2.2", // last updated 04/21/2020
166 | "SWIFT_BASE_REPO_NAME": "swift",
167 | "SWIFT_BASE_VERSION": "${SWIFT_VERSION}",
168 | ],
169 |
170 | // MARK: - Vapor Swift repo, `vapor/swift` prefix, duplicate declaration to make the "latest" tag without permutation.
171 | .init(name: "vapor/swift", defaultDockerfile: "swift.Dockerfile",
172 | replacements: ["REPOSITORY_NAME": "vapor/swift", "IMAGE_OS_VERSION": ""],
173 | template: commonSwiftImageTemplate,
174 | .init("SWIFT_VERSION", .value("latest")),
175 | .init("IMAGE_VAPOR_VARIANT", .empty, .valueAndKeys("ci", ["CURL_DEPENDENCY": "curl"]))
176 | ),
177 | // MARK: - Vapor Swift repo, `vapor/swift` prefix
178 | .init(
179 | name: "vapor/swift",
180 | defaultDockerfile: "swift.Dockerfile",
181 | replacements: ["REPOSITORY_NAME": "vapor/swift"],
182 | template: commonSwiftImageTemplate,
183 |
184 | // Vapor 4-compatible Swift versions we build.
185 | .init("SWIFT_VERSION",
186 | // Build latest release version and aliases for it, including "latest".
187 | // Master is not latest; it's a nightly.
188 | .value("${SWIFT_LATEST_RELEASE_VERSION}${:chopVersion}"),
189 | .value("${SWIFT_LATEST_RELEASE_VERSION}"),
190 |
191 | // Build swiftlang/nightly-master as master.
192 | .valueAndKeys("master", ["SWIFT_BASE_REPO_NAME": "swiftlang/swift", "SWIFT_BASE_VERSION": "nightly-master"])
193 | ),
194 | // Swift Ubuntu OS version variant set - none (bionic by default), bionic, and xenial.
195 | .init("IMAGE_OS_VERSION", .empty, .value("bionic"), .value("xenial")),
196 | // Image build purpose variant set - standard (no extra tag) and CI (requiring curl installed)
197 | .init("IMAGE_VAPOR_VARIANT", .empty, .valueAndKeys("ci", ["CURL_DEPENDENCY": "curl"]))
198 | ),
199 |
200 | // MARK: - Vapor3 legacy org and swift repo, vapor3/swift* images
201 | .init(
202 | name: "vapor3/swift",
203 | defaultDockerfile: "swift.Dockerfile",
204 | replacements: ["REPOSITORY_NAME": "vapor3/swift", "LIBSSL_DEPENDENCY": "libssl-dev"],
205 | template: commonSwiftImageTemplate,
206 | // Build a couple of older ones and their aliases.
207 | .init("SWIFT_VERSION",
208 | .value("${SWIFT_LATEST_RELEASE_VERSION}${:chopVersion}"), .value("${SWIFT_LATEST_RELEASE_VERSION}"),
209 | .value("5.1.5"), .value("5.1"),
210 | .value("5.0.3"), .value("5.0")
211 | ),
212 | .init("IMAGE_OS_VERSION", .empty, .value("xenial"), .value("bionic")),
213 | .init("IMAGE_VAPOR_VARIANT", .empty, .valueAndKeys("ci", ["CURL_DEPENDENCY": "curl"]))
214 | ),
215 |
216 | // MARK: - Ubuntu repo, `vapor/ubuntu` prefix
217 | .init(
218 | name: "vapor/ubuntu",
219 | defaultDockerfile: "ubuntu.Dockerfile",
220 | template: .init(
221 | nameTemplate: "vapor/ubuntu:${UBUNTU_OS_IMAGE_VERSION}",
222 | buildArguments: [
223 | "UBUNTU_OS_IMAGE_VERSION": "${UBUNTU_OS_IMAGE_VERSION}",
224 | "UBUNTU_VERSION_SPECIFIC_APT_DEPENDENCIES": "${UBUNTU_VERSION_SPECIFIC_APT_DEPENDENCIES}"
225 | ]
226 | ),
227 |
228 | // Build images for xenial and bionic, and provide version number aliases.
229 | .init("UBUNTU_OS_IMAGE_VERSION",
230 | .valueAndKeys("16.04", ubuntuXenialDeps), .valueAndKeys("xenial", ubuntuXenialDeps),
231 | .valueAndKeys("18.04", ubuntuBionicDeps), .valueAndKeys("bionic", ubuntuBionicDeps)
232 | )
233 | )
234 |
235 | ) }
236 | }
237 |
--------------------------------------------------------------------------------