Add build.cpus and build.memory system properties (#1293)

This commit is contained in:
Raj
2026-03-05 17:53:47 -08:00
committed by GitHub
parent 0557b83b0e
commit 926b3a3a23
6 changed files with 108 additions and 35 deletions
+2 -2
View File
@@ -67,7 +67,7 @@ extension Application {
}()
@Option(name: .shortAndLong, help: "Number of CPUs to allocate to the builder container")
var cpus: Int64 = 2
var cpus: Int64?
@Option(name: .shortAndLong, help: ArgumentHelp("Path to Dockerfile", valueName: "path"))
var file: String?
@@ -79,7 +79,7 @@ extension Application {
name: .shortAndLong,
help: "Amount of builder container memory (1MiByte granularity), with optional K, M, G, T, or P suffix"
)
var memory: String = "2048MB"
var memory: String?
@Flag(name: .long, help: "Do not use cache")
var noCache: Bool = false
@@ -29,6 +29,9 @@ import TerminalProgress
extension Application {
public struct BuilderStart: AsyncLoggableCommand {
static let defaultCPUs = 2
static let defaultMemoryInBytes: UInt64 = 2048.mib()
public static var configuration: CommandConfiguration {
var config = CommandConfiguration()
config.commandName = "start"
@@ -37,13 +40,13 @@ extension Application {
}
@Option(name: .shortAndLong, help: "Number of CPUs to allocate to the builder container")
var cpus: Int64 = 2
var cpus: Int64?
@Option(
name: .shortAndLong,
help: "Amount of builder container memory (1MiByte granularity), with optional K, M, G, T, or P suffix"
)
var memory: String = "2048MB"
var memory: String?
@OptionGroup
public var dns: Flags.DNS
@@ -135,23 +138,16 @@ extension Application {
// Check if we need to recreate the builder due to different image
let imageChanged = existingImage != builderImage
let cpuChanged = {
if let cpus {
if existingResources.cpus != cpus {
return true
}
}
return false
}()
let memChanged = try {
if let memory {
let memoryInBytes = try Parser.resources(cpus: nil, memory: memory).memoryInBytes
if existingResources.memoryInBytes != memoryInBytes {
return true
}
}
return false
}()
let resolvedResources = try Parser.resources(
cpus: cpus,
memory: memory,
cpuPropertyKey: .defaultBuildCPUs,
memoryPropertyKey: .defaultBuildMemory,
defaultCPUs: Self.defaultCPUs,
defaultMemoryInBytes: Self.defaultMemoryInBytes
)
let cpuChanged = existingResources.cpus != resolvedResources.cpus
let memChanged = existingResources.memoryInBytes != resolvedResources.memoryInBytes
let dnsChanged = {
if !dnsNameservers.isEmpty {
return existingDNS?.nameservers != dnsNameservers
@@ -241,7 +237,11 @@ extension Application {
let resources = try Parser.resources(
cpus: cpus,
memory: memory
memory: memory,
cpuPropertyKey: .defaultBuildCPUs,
memoryPropertyKey: .defaultBuildMemory,
defaultCPUs: Self.defaultCPUs,
defaultMemoryInBytes: Self.defaultMemoryInBytes
)
var config = ContainerConfiguration(id: Builder.builderContainerId, image: imageDesc, process: processConfig)
@@ -51,6 +51,16 @@ extension Application {
throw ContainerizationError(.invalidArgument, message: "invalid boolean value: \(value)")
}
DefaultsStore.setBool(value: boolValue, key: key)
case .defaultBuildCPUs, .defaultContainerCPUs:
guard let cpuCount = Int(value), cpuCount > 0 else {
throw ContainerizationError(.invalidArgument, message: "invalid CPU count: \(value)")
}
DefaultsStore.set(value: value, key: key)
case .defaultBuildMemory, .defaultContainerMemory:
guard let memoryMiB = try? Parser.memoryStringAsMiB(value), memoryMiB > 0 else {
throw ContainerizationError(.invalidArgument, message: "invalid memory value: \(value)")
}
DefaultsStore.set(value: value, key: key)
case .defaultDNSDomain, .defaultRegistryDomain:
guard Parser.isValidDomainName(value) else {
throw ContainerizationError(.invalidArgument, message: "invalid domain name: \(value)")
@@ -69,16 +79,6 @@ extension Application {
}
DefaultsStore.set(value: value, key: key)
return
case .defaultContainerCPUs:
guard let cpuCount = Int(value), cpuCount > 0 else {
throw ContainerizationError(.invalidArgument, message: "invalid CPU count: \(value)")
}
DefaultsStore.set(value: value, key: key)
case .defaultContainerMemory:
guard let memoryMiB = try? Parser.memoryStringAsMiB(value), memoryMiB > 0 else {
throw ContainerizationError(.invalidArgument, message: "invalid memory value: \(value)")
}
DefaultsStore.set(value: value, key: key)
case .defaultSubnet:
guard (try? CIDRv4(value)) != nil else {
throw ContainerizationError(.invalidArgument, message: "invalid CIDRv4 address: \(value)")
@@ -24,6 +24,8 @@ public enum DefaultsStore {
public enum Keys: String {
case buildRosetta = "build.rosetta"
case defaultBuildCPUs = "build.cpus"
case defaultBuildMemory = "build.memory"
case defaultContainerCPUs = "container.cpus"
case defaultContainerMemory = "container.memory"
case defaultDNSDomain = "dns.domain"
@@ -71,6 +73,8 @@ public enum DefaultsStore {
public static func allValues() -> [DefaultsStoreValue] {
let allKeys: [(Self.Keys, (Self.Keys) -> Any?)] = [
(.buildRosetta, { Self.getBool(key: $0) }),
(.defaultBuildCPUs, { Self.getOptional(key: $0) }),
(.defaultBuildMemory, { Self.getOptional(key: $0) }),
(.defaultContainerCPUs, { Self.getOptional(key: $0) }),
(.defaultContainerMemory, { Self.getOptional(key: $0) }),
(.defaultBuilderImage, { Self.get(key: $0) }),
@@ -126,6 +130,10 @@ extension DefaultsStore.Keys {
switch self {
case .buildRosetta:
return "Build amd64 images on arm64 using Rosetta, instead of QEMU."
case .defaultBuildCPUs:
return "If defined, the default number of CPUs to allocate to the builder container."
case .defaultBuildMemory:
return "If defined, the default amount of memory to allocate to the builder container."
case .defaultContainerCPUs:
return "If defined, the default number of CPUs to allocate to a container."
case .defaultContainerMemory:
@@ -153,6 +161,10 @@ extension DefaultsStore.Keys {
switch self {
case .buildRosetta:
return Bool.self
case .defaultBuildCPUs:
return String.self
case .defaultBuildMemory:
return String.self
case .defaultContainerCPUs:
return String.self
case .defaultContainerMemory:
@@ -181,6 +193,12 @@ extension DefaultsStore.Keys {
case .buildRosetta:
// This is a boolean key, not used with the string get() method
return "true"
case .defaultBuildCPUs:
// This key is read with getOptional(), not get(); this value is never used
return "2"
case .defaultBuildMemory:
// This key is read with getOptional(), not get(); this value is never used
return "2048MB"
case .defaultContainerCPUs:
// This key is read with getOptional(), not get(); this value is never used
return "4"
@@ -87,18 +87,28 @@ public struct Parser {
try .init(from: platform)
}
public static func resources(cpus: Int64?, memory: String?) throws -> ContainerConfiguration.Resources {
public static func resources(
cpus: Int64?,
memory: String?,
cpuPropertyKey: DefaultsStore.Keys = .defaultContainerCPUs,
memoryPropertyKey: DefaultsStore.Keys = .defaultContainerMemory,
defaultCPUs: Int = 4,
defaultMemoryInBytes: UInt64 = 1024.mib()
) throws -> ContainerConfiguration.Resources {
var resource = ContainerConfiguration.Resources()
resource.cpus = defaultCPUs
resource.memoryInBytes = defaultMemoryInBytes
if let cpus {
resource.cpus = Int(cpus)
} else if let cpuStr = DefaultsStore.getOptional(key: .defaultContainerCPUs),
} else if let cpuStr = DefaultsStore.getOptional(key: cpuPropertyKey),
let cpuVal = Int(cpuStr), cpuVal > 0
{
resource.cpus = cpuVal
}
if let memory {
resource.memoryInBytes = try Parser.memoryStringAsMiB(memory).mib()
} else if let memStr = DefaultsStore.getOptional(key: .defaultContainerMemory) {
} else if let memStr = DefaultsStore.getOptional(key: memoryPropertyKey) {
resource.memoryInBytes = try Parser.memoryStringAsMiB(memStr).mib()
}
return resource
@@ -1047,6 +1047,34 @@ struct ParserTest {
// MARK: - Parser.resources
@Test
func testResourcesCustomDefaults() throws {
let result = try Parser.resources(
cpus: nil, memory: nil,
cpuPropertyKey: .defaultBuildCPUs, memoryPropertyKey: .defaultBuildMemory,
defaultCPUs: 2, defaultMemoryInBytes: 2048.mib()
)
#expect(result.cpus == 2)
#expect(result.memoryInBytes == 2048.mib())
}
@Test
func testResourcesBuildPropertyLookup() throws {
DefaultsStore.set(value: "8", key: .defaultBuildCPUs)
DefaultsStore.set(value: "4g", key: .defaultBuildMemory)
defer {
DefaultsStore.unset(key: .defaultBuildCPUs)
DefaultsStore.unset(key: .defaultBuildMemory)
}
let result = try Parser.resources(
cpus: nil, memory: nil,
cpuPropertyKey: .defaultBuildCPUs, memoryPropertyKey: .defaultBuildMemory,
defaultCPUs: 2, defaultMemoryInBytes: 2048.mib()
)
#expect(result.cpus == 8)
#expect(result.memoryInBytes == 4096.mib())
}
@Test
func testResourcesCPUsFromProperty() throws {
DefaultsStore.set(value: "8", key: .defaultContainerCPUs)
@@ -1075,4 +1103,21 @@ struct ParserTest {
#expect(result.cpus == 1)
#expect(result.memoryInBytes == 256.mib())
}
@Test
func testResourcesPropertyKeysAreIsolated() throws {
DefaultsStore.set(value: "16", key: .defaultContainerCPUs)
DefaultsStore.set(value: "8g", key: .defaultContainerMemory)
defer {
DefaultsStore.unset(key: .defaultContainerCPUs)
DefaultsStore.unset(key: .defaultContainerMemory)
}
let result = try Parser.resources(
cpus: nil, memory: nil,
cpuPropertyKey: .defaultBuildCPUs, memoryPropertyKey: .defaultBuildMemory,
defaultCPUs: 2, defaultMemoryInBytes: 2048.mib()
)
#expect(result.cpus == 2)
#expect(result.memoryInBytes == 2048.mib())
}
}