├── .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 | Documentation 4 | 5 | 6 | Team Chat 7 | 8 | 9 | MIT License 10 | 11 | 12 | Vapor images on Docker Hub 13 | 14 | 15 | Swift 5.2 16 | 17 | 18 | Twitter 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 |

logo

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 | --------------------------------------------------------------------------------