@@ -683,27 +683,16 @@ public struct ComposeUp: AsyncParsableCommand, @unchecked Sendable {
683683
684684// MARK: Static Helpers for Testing
685685
686- /// Resolves platform for build/run from service.platform or CONTAINER_DEFAULT_PLATFORM env var.
687- /// - Parameters:
688- /// - servicePlatform: Optional platform string from service configuration
689- /// - environment: Environment dictionary to read CONTAINER_DEFAULT_PLATFORM from (defaults to process environment)
690- /// - Returns: (os, arch) tuple
691- public static func resolvePlatform(
692- servicePlatform: String ? ,
693- environment: [ String : String ] = ProcessInfo . processInfo. environment
694- ) -> ( os: String , arch: String ) {
695- let platform = servicePlatform
696- ?? environment [ " CONTAINER_DEFAULT_PLATFORM " ]
697-
698- if let platform = platform {
699- let split = platform. split ( separator: " / " )
700- let os = String ( split. first ?? " linux " )
701- let arch = String ( split. count >= 2 ? split. last! : " arm64 " )
702- return ( os, arch)
703- }
704-
705- // Default fallback
706- return ( " linux " , " arm64 " )
686+ /// Resolves platform for build/run from service.platform.
687+ /// Note: Apple Container 0.11.0+ natively supports CONTAINER_DEFAULT_PLATFORM env var
688+ /// when --platform is not specified. We pass through service.platform if set.
689+ /// - Parameters:
690+ /// - servicePlatform: Optional platform string from service configuration
691+ /// - Returns: Platform string for --platform flag, or nil to use upstream defaults
692+ public static func resolvePlatform( servicePlatform: String ? ) -> String ? {
693+ // If service.platform is set, use it directly
694+ // Otherwise return nil to let upstream handle CONTAINER_DEFAULT_PLATFORM
695+ return servicePlatform
707696 }
708697
709698 public static func makeNetworkCreateArgs( name: String , config: Network ? ) -> [ String ] {
@@ -902,37 +891,53 @@ public static func resolvePlatform(
902891 if recover {
903892 // Handle different container states in recovery mode
904893 switch existingContainer. status {
905- case . running:
906- print ( " [RECOVER] Container ' \( containerName) ' is already running - skipping creation " )
907- // External Dependency Health-Gating: Record this service as externally present
908- // so that dependent services skip their service_healthy wait (crash recovery).
909- externallyPresentServices. insert ( serviceName)
910-
911- // Check for configuration drift
912- if let driftWarnings = checkContainerDrift ( container: existingContainer, service: service, expectedImage: imageToRun, env: combinedEnv) , !driftWarnings. isEmpty {
913- for warning in driftWarnings {
914- print ( " ⚠️ [DRIFT WARNING] Container ' \( containerName) ': \( warning) " )
915- }
916- }
917-
918- try await updateEnvironmentWithServiceIP ( serviceName, containerName: containerName, ports: service. ports)
919- return
920-
921- case . stopped:
922- print ( " [RECOVER] Container ' \( containerName) ' is stopped - starting it " )
923-
924- // Check for configuration drift before starting
925- if let driftWarnings = checkContainerDrift ( container: existingContainer, service: service, expectedImage: imageToRun, env: combinedEnv) , !driftWarnings. isEmpty {
926- for warning in driftWarnings {
927- print ( " ⚠️ [DRIFT WARNING] Container ' \( containerName) ': \( warning) " )
928- }
929- }
894+ case . running:
895+ print ( " [RECOVER] Container ' \( containerName) ' is already running - checking image... " )
896+
897+ // Check if image needs to be pulled (M-5: --recover image re-pull)
898+ // Even in recover mode, we should ensure the image is available locally
899+ // in case it was pruned or never pulled on this host
900+ if let image = service. image {
901+ try await pullImage ( image, platform: service. platform, scheme: service. scheme)
902+ }
903+
904+ // External Dependency Health-Gating: Record this service as externally present
905+ // so that dependent services skip their service_healthy wait (crash recovery).
906+ externallyPresentServices. insert ( serviceName)
907+
908+ // Check for configuration drift
909+ if let driftWarnings = checkContainerDrift ( container: existingContainer, service: service, expectedImage: imageToRun, env: combinedEnv) , !driftWarnings. isEmpty {
910+ for warning in driftWarnings {
911+ print ( " ⚠️ [DRIFT WARNING] Container ' \( containerName) ': \( warning) " )
912+ }
913+ }
914+
915+ try await updateEnvironmentWithServiceIP ( serviceName, containerName: containerName, ports: service. ports)
916+ print ( " [RECOVER] Container ' \( containerName) ' kept running (image pulled if needed) " )
917+ return
930918
931- let startCommand = try Application . ContainerStart. parse ( [ containerName] )
932- try await startCommand. run ( )
933- try await waitUntilContainerIsRunning ( containerName)
934- try await updateEnvironmentWithServiceIP ( serviceName, containerName: containerName, ports: service. ports)
935- return
919+ case . stopped:
920+ print ( " [RECOVER] Container ' \( containerName) ' is stopped - checking image... " )
921+
922+ // Pull image in recover mode if available (M-5: --recover image re-pull)
923+ // Ensures the image exists locally before attempting to start
924+ if let image = service. image {
925+ try await pullImage ( image, platform: service. platform, scheme: service. scheme)
926+ }
927+
928+ // Check for configuration drift before starting
929+ if let driftWarnings = checkContainerDrift ( container: existingContainer, service: service, expectedImage: imageToRun, env: combinedEnv) , !driftWarnings. isEmpty {
930+ for warning in driftWarnings {
931+ print ( " ⚠️ [DRIFT WARNING] Container ' \( containerName) ': \( warning) " )
932+ }
933+ }
934+
935+ print ( " [RECOVER] Starting container ' \( containerName) ' " )
936+ let startCommand = try Application . ContainerStart. parse ( [ containerName] )
937+ try await startCommand. run ( )
938+ try await waitUntilContainerIsRunning ( containerName)
939+ try await updateEnvironmentWithServiceIP ( serviceName, containerName: containerName, ports: service. ports)
940+ return
936941
937942 default :
938943 // Zombie container states: creating, dead, restarting, etc.
@@ -1143,7 +1148,7 @@ public static func resolvePlatform(
11431148 let imagePull = try Application . ImagePull. parse ( pullCommands)
11441149 try await imagePull. run ( )
11451150 } catch {
1146- if let scheme = scheme, isUnknownOptionError ( error) {
1151+ if let scheme = scheme, Self . detectUnknownOptionError ( error) {
11471152 print ( " ⚠️ Warning: Apple Container runtime does not support '--scheme \( scheme) ' flag. " )
11481153 print ( " Pulling image without scheme override... " )
11491154 var fallbackCommands = pullCommands. filter { $0 != " --scheme " && ( pullCommands. firstIndex ( of: $0) . map { pullCommands [ $0 + 1 ] == scheme } ?? false ) == false }
@@ -1156,14 +1161,14 @@ public static func resolvePlatform(
11561161 }
11571162 }
11581163
1159- private static func isUnknownOptionError ( _ error: Error ) -> Bool {
1164+ private static func detectUnknownOptionError ( _ error: Error ) -> Bool {
11601165 let errorString = String ( describing: error)
11611166 return errorString. contains ( " unknownOption " ) || errorString. contains ( " unknown option " ) || errorString. contains ( " unrecognized option " ) || errorString. contains ( " 未知的选项 " )
11621167 }
11631168
11641169 private static func isSchemeUnsupportedError( _ error: Error , scheme: String ? ) -> Bool {
11651170 guard scheme != nil else { return false }
1166- return isUnknownOptionError ( error)
1171+ return Self . detectUnknownOptionError ( error)
11671172 }
11681173
11691174 /// Builds Docker Service
@@ -1212,10 +1217,12 @@ public static func resolvePlatform(
12121217 commands. append ( " --no-cache " )
12131218 }
12141219
1215- // Add OS/Arch
1216- let ( os, arch) = Self . resolvePlatform ( servicePlatform: service. platform)
1217- commands. append ( contentsOf: [ " --os " , os] )
1218- commands. append ( contentsOf: [ " --arch " , arch] )
1220+ // Add platform (Apple Container 0.11.0+ natively supports CONTAINER_DEFAULT_PLATFORM env var)
1221+ // Only pass --platform if service.platform is explicitly set
1222+ if let platform = service. platform {
1223+ commands. append ( contentsOf: [ " --platform " , platform] )
1224+ }
1225+ // Otherwise let upstream handle CONTAINER_DEFAULT_PLATFORM or use defaults
12191226
12201227 // Add image name
12211228 commands. append ( contentsOf: [ " --tag " , imageToRun] )
@@ -1229,23 +1236,12 @@ public static func resolvePlatform(
12291236 commands. append ( contentsOf: [ " --cpus " , " \( cpuCount) " ] )
12301237 commands. append ( contentsOf: [ " --memory " , memoryLimit] )
12311238
1239+ let buildCommand = try Application . BuildCommand. parse ( commands)
12321240 print ( " \n ---------------------------------------- " )
12331241 print ( " Building image for service: \( serviceName) (Tag: \( imageToRun) ) " )
12341242 print ( " Running: container build \( commands. joined ( separator: " " ) ) " )
1235-
1236- // Bypass ArgumentParser - directly invoke the container CLI via shell
1237- let exitCode = try await ContainerComposeCore . streamCommand (
1238- " container " ,
1239- args: [ " build " ] + commands,
1240- cwd: self . cwd,
1241- onStdout: { print ( $0) } ,
1242- onStderr: { print ( $0) }
1243- )
1244-
1245- if exitCode != 0 {
1246- throw ComposeError . buildFailed ( " Build command failed with exit code \( exitCode) " )
1247- }
1248-
1243+ try buildCommand. validate ( )
1244+ try await buildCommand. run ( )
12491245 print ( " Image build for \( serviceName) completed. " )
12501246 print ( " ---------------------------------------- " )
12511247
0 commit comments