From 77bf3eaf7aeafeba100089b30c18017fea02d38b Mon Sep 17 00:00:00 2001 From: Derek McGowan Date: Fri, 1 Nov 2024 16:21:16 -0700 Subject: [PATCH] Add support for OS Features in the format Updates the os part of the format to include features after the os version. The guarantees that the format may fully represent the platform structure. Signed-off-by: Derek McGowan --- platforms.go | 40 ++++++++++++++++++++++++---------------- platforms_test.go | 24 ++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 16 deletions(-) diff --git a/platforms.go b/platforms.go index 1bbbdb9..286082c 100644 --- a/platforms.go +++ b/platforms.go @@ -121,12 +121,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 @@ -177,11 +175,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 @@ -198,14 +199,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) @@ -289,8 +293,12 @@ func FormatAll(platform specs.Platform) string { return "unknown" } - if platform.OSVersion != "" { - OSAndVersion := fmt.Sprintf(osAndVersionFormat, platform.OS, platform.OSVersion) + osOptions := platform.OSVersion + for _, feature := range platform.OSFeatures { + osOptions += "+" + feature + } + 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..00d20a2 100644 --- a/platforms_test.go +++ b/platforms_test.go @@ -343,6 +343,30 @@ func TestParseSelector(t *testing.T) { formatted: path.Join("windows(10.0.17763)", 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, + }, } { t.Run(testcase.input, func(t *testing.T) { if testcase.skip {