@@ -18,11 +18,11 @@ import ArgumentParser
1818import ContainerAPIClient
1919import ContainerPersistence
2020import ContainerPlugin
21+ import ContainerResource
2122import Containerization
2223import ContainerizationError
2324import ContainerizationOCI
2425import Foundation
25- import SwiftProtobuf
2626
2727extension Application {
2828 public struct ImageList : AsyncLoggableCommand {
@@ -53,27 +53,30 @@ extension Application {
5353 }
5454 images. sort { $0. reference < $1. reference }
5555
56- if format == . json {
57- try await Self . emitJSON ( images: images)
58- return
59- }
60-
61- if quiet {
56+ // Quiet mode prints references directly and skips the more expensive
57+ // per-image manifest resolution. `--format json` takes precedence.
58+ if quiet && format != . json {
6259 for image in images {
6360 let processedReferenceString = try ClientImage . denormalizeReference ( image. reference, containerSystemConfig: containerSystemConfig)
6461 print ( processedReferenceString)
6562 }
6663 return
6764 }
6865
66+ let resources = try await Self . buildResources ( images: images, containerSystemConfig: containerSystemConfig)
67+
68+ if format == . json {
69+ try Self . emitJSON ( resources: resources)
70+ return
71+ }
72+
6973 if verbose {
70- let items = try await Self . buildVerboseItems ( images : images , containerSystemConfig : containerSystemConfig )
71- Output . emit ( Output . renderTable ( items ) )
74+ let rows = resources . flatMap { VerboseImageRow . rows ( for : $0 ) }
75+ Output . emit ( Output . renderTable ( rows ) )
7276 return
7377 }
7478
75- let items = try await Self . buildTableItems ( images: images, containerSystemConfig: containerSystemConfig)
76- Output . emit ( Output . renderTable ( items) )
79+ Output . emit ( Output . renderTable ( resources) )
7780 }
7881
7982 private static func validate( quiet: Bool , verbose: Bool ) throws {
@@ -82,111 +85,27 @@ extension Application {
8285 }
8386 }
8487
85- private static func emitJSON( images: [ ClientImage ] ) async throws {
86- let formatter = ByteCountFormatter ( )
87- var printableImages : [ PrintableImage ] = [ ]
88+ /// Builds the resource for each image, denormalizing the reference so the
89+ /// display name omits the default registry.
90+ private static func buildResources( images: [ ClientImage ] , containerSystemConfig: ContainerSystemConfig ) async throws -> [ ImageResource ] {
91+ var resources : [ ImageResource ] = [ ]
8892 for image in images {
89- let size = try await ClientImage . getFullImageSize ( image: image)
90- let formattedSize = formatter. string ( fromByteCount: size)
91- printableImages. append (
92- PrintableImage ( reference: image. reference, fullSize: formattedSize, descriptor: image. descriptor)
93- )
94- }
95- try Output . emit ( Output . renderJSON ( printableImages) )
96- }
97-
98- private static func buildTableItems( images: [ ClientImage ] , containerSystemConfig: ContainerSystemConfig ) async throws -> [ ImageRow ] {
99- var items : [ ImageRow ] = [ ]
100- for image in images {
101- let processedReferenceString = try ClientImage . denormalizeReference ( image. reference, containerSystemConfig: containerSystemConfig)
102- let reference = try ContainerizationOCI . Reference. parse ( processedReferenceString)
103- let digest = try await image. resolved ( ) . digest
104- items. append (
105- ImageRow (
106- name: reference. name,
107- tag: reference. tag ?? " <none> " ,
108- trimmedDigest: Utility . trimDigest ( digest: digest)
109- ) )
110- }
111- return items
112- }
113-
114- private static func buildVerboseItems( images: [ ClientImage ] , containerSystemConfig: ContainerSystemConfig ) async throws -> [ VerboseImageRow ] {
115- let formatter = ByteCountFormatter ( )
116- var items : [ VerboseImageRow ] = [ ]
117- for image in images {
118- let imageDigest = try await image. resolved ( ) . digest
119- let processedReferenceString = try ClientImage . denormalizeReference ( image. reference, containerSystemConfig: containerSystemConfig)
120- let reference = try ContainerizationOCI . Reference. parse ( processedReferenceString)
121- for descriptor in try await image. index ( ) . manifests {
122- if let referenceType = descriptor. annotations ? [ " vnd.docker.reference.type " ] ,
123- referenceType == " attestation-manifest "
124- {
125- continue
126- }
127-
128- guard let platform = descriptor. platform else {
129- continue
130- }
131-
132- var config : ContainerizationOCI . Image
133- var manifest : ContainerizationOCI . Manifest
134- do {
135- config = try await image. config ( for: platform)
136- manifest = try await image. manifest ( for: platform)
137- } catch {
138- continue
139- }
140-
141- let created = config. created ?? " "
142- let size = descriptor. size + manifest. config. size + manifest. layers. reduce ( 0 ) { $0 + $1. size }
143- let formattedSize = formatter. string ( fromByteCount: size)
144-
145- items. append (
146- VerboseImageRow (
147- name: reference. name,
148- tag: reference. tag ?? " <none> " ,
149- indexDigest: Utility . trimDigest ( digest: imageDigest) ,
150- os: platform. os,
151- arch: platform. architecture,
152- variant: platform. variant ?? " " ,
153- fullSize: formattedSize,
154- created: created,
155- manifestDigest: Utility . trimDigest ( digest: descriptor. digest)
156- ) )
157- }
93+ let resolved = try await image. resolvedManifests ( )
94+ let displayReference = try ClientImage . denormalizeReference ( image. reference, containerSystemConfig: containerSystemConfig)
95+ resources. append (
96+ ImageResource ( config: image. description, index: resolved. index, manifests: resolved. manifests, displayReference: displayReference) )
15897 }
159- return items
98+ return resources
16099 }
161100
162- struct PrintableImage: Codable {
163- let reference : String
164- let fullSize : String
165- let descriptor : Descriptor
101+ private static func emitJSON( resources: [ ImageResource ] ) throws {
102+ let options = JSONOptions ( dateEncodingStrategy: . iso8601)
103+ try Output . emit ( Output . renderJSON ( resources, options: options) )
166104 }
167105 }
168106}
169107
170- private struct ImageRow : ListDisplayable {
171- let name : String
172- let tag : String
173- let trimmedDigest : String
174-
175- static var tableHeader : [ String ] {
176- [ " NAME " , " TAG " , " DIGEST " ]
177- }
178-
179- var tableRow : [ String ] {
180- [ name, tag, trimmedDigest]
181- }
182-
183- // Required by ListDisplayable but unused — ImageList handles quiet mode
184- // separately to avoid expensive digest resolution.
185- var quietValue : String {
186- name
187- }
188- }
189-
108+ /// A single row of the verbose image listing — one per platform variant.
190109private struct VerboseImageRow : ListDisplayable {
191110 let name : String
192111 let tag : String
@@ -209,4 +128,30 @@ private struct VerboseImageRow: ListDisplayable {
209128 var quietValue : String {
210129 name
211130 }
131+
132+ /// Flattens an ImageResource into one verbose image row entry per platform variant.
133+ static func rows( for resource: ImageResource ) -> [ VerboseImageRow ] {
134+ let formatter = ByteCountFormatter ( )
135+ let reference = try ? ContainerizationOCI . Reference. parse ( resource. displayReference)
136+ let name = reference? . name ?? resource. displayReference
137+ let tag = reference? . tag ?? " <none> "
138+ let indexDigest = Utility . trimDigest ( digest: resource. index. digest)
139+ return
140+ resource. variants
141+ // Skip attestation manifests, which use the `unknown/unknown` platform.
142+ . filter { !( $0. platform. os == " unknown " && $0. platform. architecture == " unknown " ) }
143+ . map { variant in
144+ VerboseImageRow (
145+ name: name,
146+ tag: tag,
147+ indexDigest: indexDigest,
148+ os: variant. platform. os,
149+ arch: variant. platform. architecture,
150+ variant: variant. platform. variant ?? " " ,
151+ fullSize: formatter. string ( fromByteCount: variant. size) ,
152+ created: variant. config. created ?? " " ,
153+ manifestDigest: Utility . trimDigest ( digest: variant. digest)
154+ )
155+ }
156+ }
212157}
0 commit comments