diff --git a/go.mod b/go.mod index d110efb..50c854c 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/containerd/platforms -go 1.20 +go 1.21 require ( github.com/containerd/log v0.1.0 diff --git a/platforms.go b/platforms.go index 14d65ab..53e729f 100644 --- a/platforms.go +++ b/platforms.go @@ -114,6 +114,7 @@ import ( "path" "regexp" "runtime" + "slices" "strconv" "strings" @@ -121,12 +122,10 @@ import ( ) var ( - specifierRe = regexp.MustCompile(`^[A-Za-z0-9_.-]+$`) - osAndVersionRe = regexp.MustCompile(`^([A-Za-z0-9_-]+)(?:\(([A-Za-z0-9_.-]*)\))?$`) + specifierRe = regexp.MustCompile(`^[A-Za-z0-9_.-]+$`) + osRe = regexp.MustCompile(`^([A-Za-z0-9_-]+)(?:\(([A-Za-z0-9_.-]*)((?:\+[A-Za-z0-9_.-]+)*)\))?$`) ) -const osAndVersionFormat = "%s(%s)" - // Platform is a type alias for convenience, so there is no need to import image-spec package everywhere. type Platform = specs.Platform @@ -210,11 +209,14 @@ func ParseAll(specifiers []string) ([]specs.Platform, error) { // Parse parses the platform specifier syntax into a platform declaration. // -// Platform specifiers are in the format `[()]||[()]/[/]`. +// Platform specifiers are in the format `[()]||[()]/[/]`. // The minimum required information for a platform specifier is the operating -// system or architecture. The OSVersion can be part of the OS like `windows(10.0.17763)` -// When an OSVersion is specified, then specs.Platform.OSVersion is populated with that value, -// and an empty string otherwise. +// system or architecture. The "os options" may be OSVersion which can be part of the OS +// like `windows(10.0.17763)`. When an OSVersion is specified, then specs.Platform.OSVersion is +// populated with that value, and an empty string otherwise. The "os options" may also include an +// array of OSFeatures, each feature prefixed with '+', without any other separator, and provided +// after the OSVersion when the OSVersion is specified. An "os options" with version and features +// is like `windows(10.0.17763+win32k)`. // If there is only a single string (no slashes), the // value will be matched against the known set of operating systems, then fall // back to the known set of architectures. The missing component will be @@ -231,14 +233,17 @@ func Parse(specifier string) (specs.Platform, error) { var p specs.Platform for i, part := range parts { if i == 0 { - // First element is [()] - osVer := osAndVersionRe.FindStringSubmatch(part) - if osVer == nil { - return specs.Platform{}, fmt.Errorf("%q is an invalid OS component of %q: OSAndVersion specifier component must match %q: %w", part, specifier, osAndVersionRe.String(), errInvalidArgument) + // First element is [([+]*)] + osOptions := osRe.FindStringSubmatch(part) + if osOptions == nil { + return specs.Platform{}, fmt.Errorf("%q is an invalid OS component of %q: OSAndVersion specifier component must match %q: %w", part, specifier, osRe.String(), errInvalidArgument) } - p.OS = normalizeOS(osVer[1]) - p.OSVersion = osVer[2] + p.OS = normalizeOS(osOptions[1]) + p.OSVersion = osOptions[2] + if osOptions[3] != "" { + p.OSFeatures = strings.Split(osOptions[3][1:], "+") + } } else { if !specifierRe.MatchString(part) { return specs.Platform{}, fmt.Errorf("%q is an invalid component of %q: platform specifier component must match %q: %w", part, specifier, specifierRe.String(), errInvalidArgument) @@ -322,8 +327,17 @@ func FormatAll(platform specs.Platform) string { return "unknown" } - if platform.OSVersion != "" { - OSAndVersion := fmt.Sprintf(osAndVersionFormat, platform.OS, platform.OSVersion) + osOptions := platform.OSVersion + features := platform.OSFeatures + if !slices.IsSorted(features) { + features = slices.Clone(features) + slices.Sort(features) + } + if len(features) > 0 { + osOptions += "+" + strings.Join(features, "+") + } + if osOptions != "" { + OSAndVersion := fmt.Sprintf("%s(%s)", platform.OS, osOptions) return path.Join(OSAndVersion, platform.Architecture, platform.Variant) } return path.Join(platform.OS, platform.Architecture, platform.Variant) diff --git a/platforms_test.go b/platforms_test.go index 8a26f5c..c9e8326 100644 --- a/platforms_test.go +++ b/platforms_test.go @@ -343,6 +343,54 @@ func TestParseSelector(t *testing.T) { formatted: path.Join("windows(10.0.17763)", defaultArch, defaultVariant), useV2Format: true, }, + { + input: "windows(10.0.17763+win32k)", + expected: specs.Platform{ + OS: "windows", + OSVersion: "10.0.17763", + OSFeatures: []string{"win32k"}, + Architecture: defaultArch, + Variant: defaultVariant, + }, + formatted: path.Join("windows(10.0.17763+win32k)", defaultArch, defaultVariant), + useV2Format: true, + }, + { + input: "linux(+gpu)", + expected: specs.Platform{ + OS: "linux", + OSVersion: "", + OSFeatures: []string{"gpu"}, + Architecture: defaultArch, + Variant: defaultVariant, + }, + formatted: path.Join("linux(+gpu)", defaultArch, defaultVariant), + useV2Format: true, + }, + { + input: "linux(+gpu+simd)", + expected: specs.Platform{ + OS: "linux", + OSVersion: "", + OSFeatures: []string{"gpu", "simd"}, + Architecture: defaultArch, + Variant: defaultVariant, + }, + formatted: path.Join("linux(+gpu+simd)", defaultArch, defaultVariant), + useV2Format: true, + }, + { + input: "linux(+unsorted+erofs)", + expected: specs.Platform{ + OS: "linux", + OSVersion: "", + OSFeatures: []string{"unsorted", "erofs"}, + Architecture: defaultArch, + Variant: defaultVariant, + }, + formatted: path.Join("linux(+erofs+unsorted)", defaultArch, defaultVariant), + useV2Format: true, + }, } { t.Run(testcase.input, func(t *testing.T) { if testcase.skip {