@@ -33,6 +33,7 @@ import (
3333 "oras.land/oras/cmd/oras/internal/argument"
3434 "oras.land/oras/cmd/oras/internal/command"
3535 "oras.land/oras/cmd/oras/internal/display"
36+ "oras.land/oras/cmd/oras/internal/display/metadata"
3637 "oras.land/oras/cmd/oras/internal/display/status"
3738 oerrors "oras.land/oras/cmd/oras/internal/errors"
3839 "oras.land/oras/cmd/oras/internal/option"
@@ -85,6 +86,9 @@ Example - Copy an artifact and referrers using specific methods for the Referrer
8586Example - Copy certain platform of an artifact:
8687 oras cp --platform linux/arm/v5 localhost:5000/net-monitor:v1 localhost:6000/net-monitor-copy:v1
8788
89+ Example - Copy certain platforms of an artifact:
90+ oras cp --platform linux/amd64,linux/arm64,linux/arm/v7 localhost:5000/net-monitor:v1 localhost:6000/net-monitor-copy:v1
91+
8892Example - Copy an artifact with multiple tags:
8993 oras cp localhost:5000/net-monitor:v1 localhost:6000/net-monitor-copy:tag1,tag2,tag3
9094
@@ -138,20 +142,181 @@ func runCopy(cmd *cobra.Command, opts *copyOptions) error {
138142 ctx = registryutil .WithScopeHint (ctx , dst , auth .ActionPull , auth .ActionPush )
139143 statusHandler , metadataHandler := display .NewCopyHandler (opts .Printer , opts .TTY , dst )
140144
141- desc , err := doCopy (ctx , statusHandler , src , dst , opts )
145+ // Check if multiple platforms are specified
146+ if len (opts .Platform .Platforms ) > 1 && ! opts .recursive {
147+ // Handle multiple platforms - copy manifests that match the specified platforms
148+ return copyMultiplePlatforms (ctx , statusHandler , metadataHandler , src , dst , opts )
149+ } else {
150+ // Original behavior for single platform or recursive mode
151+ desc , err := doCopy (ctx , statusHandler , src , dst , opts )
152+ if err != nil {
153+ return err
154+ }
155+
156+ if from , err := digest .Parse (opts .From .Reference ); err == nil && from != desc .Digest {
157+ // correct source digest
158+ opts .From .RawReference = fmt .Sprintf ("%s@%s" , opts .From .Path , desc .Digest .String ())
159+ }
160+
161+ if err := metadataHandler .OnCopied (& opts .BinaryTarget , desc ); err != nil {
162+ return err
163+ }
164+
165+ if len (opts .extraRefs ) != 0 {
166+ tagNOpts := oras .DefaultTagNOptions
167+ tagNOpts .Concurrency = opts .concurrency
168+ tagListener := listener .NewTaggedListener (dst , metadataHandler .OnTagged )
169+ if _ , err = oras .TagN (ctx , tagListener , opts .To .Reference , opts .extraRefs , tagNOpts ); err != nil {
170+ return err
171+ }
172+ }
173+
174+ return metadataHandler .Render ()
175+ }
176+ }
177+
178+ // copyMultiplePlatforms handles copying when multiple platforms are specified
179+ func copyMultiplePlatforms (ctx context.Context , statusHandler status.CopyHandler , metadataHandler metadata.CopyHandler , src oras.ReadOnlyGraphTarget , dst oras.GraphTarget , opts * copyOptions ) error {
180+ // Resolve the source reference to get the root descriptor
181+ resolveOpts := oras .DefaultResolveOptions
182+ // We don't set TargetPlatform here since we want to get the full index/list
183+ root , err := oras .Resolve (ctx , src , opts .From .Reference , resolveOpts )
142184 if err != nil {
143- return err
185+ return fmt .Errorf ("failed to resolve %s: %w" , opts .From .Reference , err )
186+ }
187+
188+ // Check if the resolved descriptor is an index/manifest list
189+ isIndex := root .MediaType == ocispec .MediaTypeImageIndex || root .MediaType == docker .MediaTypeManifestList
190+ if ! isIndex {
191+ // If not an index, fall back to single platform behavior with first platform
192+ tempOpts := * opts
193+ tempOpts .Platform .Platform = opts .Platform .Platforms [0 ]
194+ desc , err := doCopy (ctx , statusHandler , src , dst , & tempOpts )
195+ if err != nil {
196+ return err
197+ }
198+
199+ if from , err := digest .Parse (opts .From .Reference ); err == nil && from != desc .Digest {
200+ opts .From .RawReference = fmt .Sprintf ("%s@%s" , opts .From .Path , desc .Digest .String ())
201+ }
202+
203+ if err := metadataHandler .OnCopied (& opts .BinaryTarget , desc ); err != nil {
204+ return err
205+ }
206+
207+ if len (opts .extraRefs ) != 0 {
208+ tagNOpts := oras .DefaultTagNOptions
209+ tagNOpts .Concurrency = opts .concurrency
210+ tagListener := listener .NewTaggedListener (dst , metadataHandler .OnTagged )
211+ if _ , err = oras .TagN (ctx , tagListener , opts .To .Reference , opts .extraRefs , tagNOpts ); err != nil {
212+ return err
213+ }
214+ }
215+
216+ return metadataHandler .Render ()
217+ }
218+
219+ // For indexes/lists, fetch the index content
220+ indexContent , err := content .FetchAll (ctx , src , root )
221+ if err != nil {
222+ return fmt .Errorf ("failed to fetch index: %w" , err )
223+ }
224+
225+ var index ocispec.Index
226+ if err := json .Unmarshal (indexContent , & index ); err != nil {
227+ return fmt .Errorf ("failed to parse index: %w" , err )
228+ }
229+
230+ // Filter manifests based on the specified platforms
231+ var filteredManifests []ocispec.Descriptor
232+ for _ , manifest := range index .Manifests {
233+ if manifest .Platform != nil && matchesAnyPlatform (manifest .Platform , opts .Platform .Platforms ) {
234+ filteredManifests = append (filteredManifests , manifest )
235+ }
236+ }
237+
238+ if len (filteredManifests ) == 0 {
239+ requestedPlatforms := opts .Platform .Platforms
240+ var availablePlatforms []string
241+ for _ , manifest := range index .Manifests {
242+ if manifest .Platform != nil {
243+ availablePlatforms = append (availablePlatforms , fmt .Sprintf ("%s/%s" , manifest .Platform .OS , manifest .Platform .Architecture ))
244+ }
245+ }
246+ availableDesc := "none"
247+ if len (availablePlatforms ) > 0 {
248+ availableDesc = strings .Join (availablePlatforms , ", " )
249+ }
250+ return fmt .Errorf ("no manifests match the requested platforms %v; available platforms in index: %s" , requestedPlatforms , availableDesc )
251+ }
252+
253+ // Create a new index with only the filtered manifests
254+ newIndex := ocispec.Index {
255+ Versioned : index .Versioned ,
256+ MediaType : index .MediaType ,
257+ Manifests : filteredManifests ,
258+ Annotations : index .Annotations ,
259+ }
260+
261+ // Marshal the new index
262+ newIndexContent , err := json .Marshal (newIndex )
263+ if err != nil {
264+ return fmt .Errorf ("failed to marshal new index: %w" , err )
144265 }
145266
146- if from , err := digest .Parse (opts .From .Reference ); err == nil && from != desc .Digest {
147- // correct source digest
148- opts .From .RawReference = fmt .Sprintf ("%s@%s" , opts .From .Path , desc .Digest .String ())
267+ // Create a descriptor for the new index
268+ newIndexDesc := ocispec.Descriptor {
269+ MediaType : index .MediaType ,
270+ Digest : digest .FromBytes (newIndexContent ),
271+ Size : int64 (len (newIndexContent )),
272+ Annotations : index .Annotations ,
149273 }
150274
151- if err := metadataHandler .OnCopied (& opts .BinaryTarget , desc ); err != nil {
275+ // Prepare copy options
276+ extendedCopyGraphOptions := oras .DefaultExtendedCopyGraphOptions
277+ extendedCopyGraphOptions .Concurrency = opts .concurrency
278+ extendedCopyGraphOptions .FindPredecessors = func (ctx context.Context , src content.ReadOnlyGraphStorage , desc ocispec.Descriptor ) ([]ocispec.Descriptor , error ) {
279+ return registry .Referrers (ctx , src , desc , "" )
280+ }
281+
282+ if mountRepo , canMount := getMountPoint (src , dst , opts ); canMount {
283+ extendedCopyGraphOptions .MountFrom = func (ctx context.Context , desc ocispec.Descriptor ) ([]string , error ) {
284+ return []string {mountRepo }, nil
285+ }
286+ }
287+ dst , err = statusHandler .StartTracking (dst )
288+ if err != nil {
152289 return err
153290 }
291+ defer func () {
292+ _ = statusHandler .StopTracking ()
293+ }()
294+ extendedCopyGraphOptions .OnCopySkipped = statusHandler .OnCopySkipped
295+ extendedCopyGraphOptions .PreCopy = statusHandler .PreCopy
296+ extendedCopyGraphOptions .PostCopy = statusHandler .PostCopy
297+ extendedCopyGraphOptions .OnMounted = statusHandler .OnMounted
298+
299+ // Copy all matching manifests and their content
300+ for _ , manifestDesc := range filteredManifests {
301+ // Copy the manifest itself
302+ if err := oras .CopyGraph (ctx , src , dst , manifestDesc , extendedCopyGraphOptions .CopyGraphOptions ); err != nil {
303+ return fmt .Errorf ("failed to copy manifest %s: %w" , manifestDesc .Digest , err )
304+ }
305+ }
306+
307+ // Push the new index to the destination
308+ if err := dst .Push (ctx , newIndexDesc , strings .NewReader (string (newIndexContent ))); err != nil {
309+ return fmt .Errorf ("failed to push new index: %w" , err )
310+ }
311+
312+ // Tag the new index if needed
313+ if opts .To .Reference != "" {
314+ if err := dst .Tag (ctx , newIndexDesc , opts .To .Reference ); err != nil {
315+ return fmt .Errorf ("failed to tag new index: %w" , err )
316+ }
317+ }
154318
319+ // Handle extra references
155320 if len (opts .extraRefs ) != 0 {
156321 tagNOpts := oras .DefaultTagNOptions
157322 tagNOpts .Concurrency = opts .concurrency
@@ -161,9 +326,47 @@ func runCopy(cmd *cobra.Command, opts *copyOptions) error {
161326 }
162327 }
163328
329+ // Update reference if needed
330+ if from , err := digest .Parse (opts .From .Reference ); err == nil && from != newIndexDesc .Digest {
331+ opts .From .RawReference = fmt .Sprintf ("%s@%s" , opts .From .Path , newIndexDesc .Digest .String ())
332+ }
333+
334+ if err := metadataHandler .OnCopied (& opts .BinaryTarget , newIndexDesc ); err != nil {
335+ return err
336+ }
337+
164338 return metadataHandler .Render ()
165339}
166340
341+ // matchesAnyPlatform checks if a manifest platform matches any of the specified platforms
342+ func matchesAnyPlatform (manifestPlatform * ocispec.Platform , platforms []* ocispec.Platform ) bool {
343+ for _ , platform := range platforms {
344+ if platformMatches (manifestPlatform , platform ) {
345+ return true
346+ }
347+ }
348+ return false
349+ }
350+
351+ // platformMatches checks if two platforms match
352+ func platformMatches (a , b * ocispec.Platform ) bool {
353+ if a .OS != b .OS || a .Architecture != b .Architecture {
354+ return false
355+ }
356+
357+ // Variant is optional; only treat it as a mismatch if both variants are non-empty and different.
358+ if a .Variant != "" && b .Variant != "" && a .Variant != b .Variant {
359+ return false
360+ }
361+
362+ // OSVersion is optional; only treat it as a mismatch if both OSVersions are non-empty and different.
363+ if a .OSVersion != "" && b .OSVersion != "" && a .OSVersion != b .OSVersion {
364+ return false
365+ }
366+
367+ return true
368+ }
369+
167370func doCopy (ctx context.Context , copyHandler status.CopyHandler , src oras.ReadOnlyGraphTarget , dst oras.GraphTarget , opts * copyOptions ) (desc ocispec.Descriptor , err error ) {
168371 // Prepare copy options
169372 extendedCopyGraphOptions := oras .DefaultExtendedCopyGraphOptions
0 commit comments