From 444e7a0044103348e545f6ce173fd34ce55026b6 Mon Sep 17 00:00:00 2001 From: Justin Vreeland Date: Wed, 28 Aug 2024 17:15:05 -0400 Subject: [PATCH] Update the python convert script for multiple versions of python. To do this add data and vars to use for substitution as well as a subapckage and remove the unversioned python dep since it will be generated correctly as part of the build. --- pkg/convert/python/e2e_test.go | 45 +++++--- pkg/convert/python/python.go | 100 ++++++++++++++++-- pkg/convert/python/python_test.go | 17 +-- .../generated/x86_64/shbang-test-1-r1.apk | Bin 4522 -> 4593 bytes 4 files changed, 131 insertions(+), 31 deletions(-) diff --git a/pkg/convert/python/e2e_test.go b/pkg/convert/python/e2e_test.go index f8143c6cc..678aade97 100644 --- a/pkg/convert/python/e2e_test.go +++ b/pkg/convert/python/e2e_test.go @@ -50,6 +50,7 @@ func TestGenerateManifest(t *testing.T) { assert.EqualValues(t, got.Package.Epoch, 0) assert.Equal(t, got.Package.Description, "Low-level, data-driven core of boto 3.") assert.Equal(t, got.Package.Dependencies.Runtime, []string{"py" + versions[i] + "-jmespath", "py" + versions[i] + "-python-dateutil", "py" + versions[i] + "-urllib3", "python-" + versions[i]}) + assert.Equal(t, "0", got.Package.Dependencies.ProviderPriority) // Check Package.Copyright assert.Equal(t, len(got.Package.Copyright), 1) @@ -60,17 +61,40 @@ func TestGenerateManifest(t *testing.T) { "build-base", "busybox", "ca-certificates-bundle", + "py3-supported-pip", "wolfi-base", }) // Check Pipeline - assert.Equal(t, len(got.Pipeline), 3) + assert.Equal(t, 1, len(got.Pipeline)) // Check Pipeline - fetch assert.Equal(t, got.Pipeline[0].Uses, "fetch") + // Check Subpackages + assert.Equal(t, "py-versions", got.Subpackages[0].Range) + assert.Equal(t, "py3-${{vars.pypi-package}}", got.Subpackages[0].Dependencies.Provides[0]) + assert.Equal(t, "py/pip-build-install", got.Subpackages[0].Pipeline[0].Uses) + var expectedRuntimeDeps []string = []string{ + "py3.10-jmespath", + "py3.10-python-dateutil", + "py3.10-urllib3", + "python-3.10", + } + // Subpackages aren't added to this array in the same order every time causing spurious + // test failues. To fix this Just check what is seen and replace the deps with the version string. + var replacementPyVersion = got.Subpackages[0].Dependencies.Runtime[0][2:6] + for idx, dep := range expectedRuntimeDeps { + expectedRuntimeDeps[idx] = strings.Replace(dep, "3.10", replacementPyVersion, 1) + } + + assert.Equal(t, expectedRuntimeDeps, got.Subpackages[0].Dependencies.Runtime) + assert.Equal(t, "${{range.value}}", got.Subpackages[0].Dependencies.ProviderPriority) releases, ok := pythonctx.Package.Releases[pythonctx.PackageVersion] + assert.Equal(t, "python/import", got.Subpackages[0].Test.Pipeline[0].Uses) + assert.Equal(t, "${{vars.module_name}}", got.Subpackages[0].Test.Pipeline[0].With["import"]) + // If the key exists assert.True(t, ok) @@ -89,9 +113,6 @@ func TestGenerateManifest(t *testing.T) { "uri": strings.ReplaceAll(tempURI, pythonctx.PackageVersion, "${{package.version}}"), }) - // Check Pipeline - runs - assert.Equal(t, got.Pipeline[1].Uses, "python/build-wheel") - assert.Equal(t, got.Pipeline[2].Uses, "strip") } } @@ -117,11 +138,8 @@ func TestGenerateManifestPreserveURI(t *testing.T) { assert.Equal(t, got.Package.Description, "Backported and Experimental Type Hints for Python 3.8+", ) - assert.Equal(t, got.Package.Dependencies.Runtime, - []string{ - "python-" + versions[i], - }, - ) + assert.Equal(t, []string{}, got.Package.Dependencies.Runtime) + assert.Equal(t, "0", got.Package.Dependencies.ProviderPriority) // Check Package.Copyright assert.Equal(t, len(got.Package.Copyright), 1) @@ -132,11 +150,12 @@ func TestGenerateManifestPreserveURI(t *testing.T) { "build-base", "busybox", "ca-certificates-bundle", + "py3-supported-pip", "wolfi-base", }) // Check Pipeline - assert.Equal(t, len(got.Pipeline), 3) + assert.Equal(t, 1, len(got.Pipeline)) // Check Pipeline - fetch assert.Equal(t, got.Pipeline[0].Uses, "fetch") @@ -160,8 +179,8 @@ func TestGenerateManifestPreserveURI(t *testing.T) { "uri": "https://files.pythonhosted.org/packages/df/db/f35a00659bc03fec321ba8bce9420de607a1d37f8342eee1863174c69557/typing_extensions-${{package.version}}.tar.gz", }) - // Check Pipeline - runs - assert.Equal(t, got.Pipeline[1].Uses, "python/build-wheel") - assert.Equal(t, got.Pipeline[2].Uses, "strip") + // Check Tests + assert.Equal(t, "python/import", got.Test.Pipeline[0].Uses) + assert.Equal(t, "${{vars.module_name}}", got.Test.Pipeline[0].With["import"]) } } diff --git a/pkg/convert/python/python.go b/pkg/convert/python/python.go index c73f4da46..28ba96e95 100644 --- a/pkg/convert/python/python.go +++ b/pkg/convert/python/python.go @@ -273,7 +273,11 @@ func (c *PythonContext) generateManifest(ctx context.Context, pack Package, vers // Generate each field in the manifest generated.GeneratedFromComment = pack.Info.ProjectURL generated.Package = c.generatePackage(ctx, pack, version) + generated.Data = c.generateRange(ctx) + generated.Vars = c.generateVars(pack) + generated.Subpackages = c.generateSubpackages(ctx, pack) generated.Environment = c.generateEnvironment(ctx, pack) + generated.Test = c.generateTest(ctx, pack) pipelines, err := c.generatePipeline(ctx, pack, version, ghVersions) if err != nil { @@ -333,8 +337,6 @@ func (c *PythonContext) generatePackage(ctx context.Context, pack Package, versi log.Infof("[%s] Run time Deps %v", pack.Info.Name, pack.Dependencies) - pack.Dependencies = append(pack.Dependencies, "python-"+c.PythonVersion) - pkg := config.Package{ Name: fmt.Sprintf("py%s-%s", c.PythonVersion, pack.Info.Name), Version: version, @@ -342,7 +344,8 @@ func (c *PythonContext) generatePackage(ctx context.Context, pack Package, versi Description: pack.Info.Summary, Copyright: []config.Copyright{}, Dependencies: config.Dependencies{ - Runtime: pack.Dependencies, + Runtime: pack.Dependencies, + ProviderPriority: "0", }, } @@ -363,6 +366,7 @@ func (c *PythonContext) generateEnvironment(ctx context.Context, pack Package) a "build-base", "busybox", "ca-certificates-bundle", + "py3-supported-pip", "wolfi-base", } @@ -455,16 +459,90 @@ func (c *PythonContext) generatePipeline(ctx context.Context, pack Package, vers }) } - pythonBuild := config.Pipeline{ - Name: "Python Build", - Uses: "python/build-wheel", + return pipeline, nil +} + +// generateVars handles generated variables for multi version python generateSubpackages +func (c *PythonContext) generateRange(ctx context.Context) []config.RangeData { + return []config.RangeData{{ + Name: "py-versions", + Items: map[string]string{ + "3.10": "310", + "3.11": "311", + "3.12": "312", + }}, } +} - strip := config.Pipeline{ - Uses: "strip", +// Generate the vars for pypi package name and pip +// Set pypi-package and module_name to the same value because it's the most common case. +// Someone else can fix it up if the build fails +func (c *PythonContext) generateVars(pack Package) map[string]string { + return map[string]string{ + "pypi-package": pack.Info.Name, + "module_name": pack.Info.Name, } - pipeline = append(pipeline, pythonBuild) - pipeline = append(pipeline, strip) +} - return pipeline, nil +// generateSubpackages handles generating suibpackages field of the melange manifest +func (c *PythonContext) generateSubpackages(ctx context.Context, pack Package) []config.Subpackage { + log := clog.FromContext(ctx) + + log.Infof("[%s] Generating Subpackages", pack.Info.Name) + + importTest := config.Test{ + Pipeline: []config.Pipeline{config.Pipeline{ + Name: "Import Test", + Uses: "python/import", + With: map[string]string{ + "python": "python${{range.key}}", + "import": "${{vars.module_name}}", + }, + }, + }, + } + + pythonSubpackages := config.Subpackage{ + Range: "py-versions", + Name: "py${{range.key}}-${{vars.pypi-package}}", + Dependencies: config.Dependencies{ + Runtime: pack.Dependencies, + Provides: []string{"py3-${{vars.pypi-package}}"}, + ProviderPriority: "${{range.value}}", + }, + Pipeline: []config.Pipeline{config.Pipeline{ + Name: "Python Build", + Uses: "py/pip-build-install", + With: map[string]string{ + "python": "python${{range.key}}", + }, + }, + }, + Test: &importTest, + } + + return []config.Subpackage{pythonSubpackages} +} + +// generate file-level package test. When building python packages for multiple +// python versions we want to ensure that we don't generate -support packages with +// contents in /bin as well as ensuring that people installing the unversioned package +// receive on and only one version of the library +func (c *PythonContext) generateTest(ctx context.Context, pack Package) *config.Test { + log := clog.FromContext(ctx) + + log.Infof("[%s] Generating Tests", pack.Info.Name) + + importTest := config.Test{ + Pipeline: []config.Pipeline{config.Pipeline{ + Name: "Import Test", + Uses: "python/import", + With: map[string]string{ + "import": "${{vars.module_name}}", + }, + }, + }, + } + + return &importTest } diff --git a/pkg/convert/python/python_test.go b/pkg/convert/python/python_test.go index eb12b7b51..e7b283936 100644 --- a/pkg/convert/python/python_test.go +++ b/pkg/convert/python/python_test.go @@ -136,7 +136,7 @@ func TestFindDependencies(t *testing.T) { } } -// TestGeneratePackage tests when a gem has multiple licenses +// TestGeneratePackage tests when a python package has multiple licenses func TestGeneratePackage(t *testing.T) { for i := range versions { pythonctxs, err := SetupContext(versions[i]) @@ -157,11 +157,12 @@ func TestGeneratePackage(t *testing.T) { }, }, Dependencies: config.Dependencies{ - Runtime: []string{"py" + versions[i] + "-jmespath", "py" + versions[i] + "-python-dateutil", "py" + versions[i] + "-urllib3", "python-" + versions[i]}, + Runtime: []string{"py" + versions[i] + "-jmespath", "py" + versions[i] + "-python-dateutil", "py" + versions[i] + "-urllib3", "python-" + versions[i]}, + ProviderPriority: "0", }, } - assert.Equal(t, got, expected) + assert.Equal(t, expected, got) } } @@ -177,7 +178,7 @@ func SetupContext(version string) ([]*PythonContext, error) { botocorepythonctx.PackageVersion = "1.29.78" botocorepythonctx.PythonVersion = version - // Read the gem meta into + // Read the pypi meta into data, err := os.ReadFile(filepath.Join(botocoreMeta, "json")) if err != nil { return nil, err @@ -190,7 +191,7 @@ func SetupContext(version string) ([]*PythonContext, error) { } botocorepythonctx.Package = botocorePackageMeta - botocorepythonctx.Package.Dependencies = []string{"py" + version + "-jmespath", "py" + version + "-python-dateutil", "py" + version + "-urllib3"} + botocorepythonctx.Package.Dependencies = []string{"py" + version + "-jmespath", "py" + version + "-python-dateutil", "py" + version + "-urllib3", "python-" + version} jsonschemapythonctx, err := New("jsonschema") if err != nil { @@ -203,7 +204,7 @@ func SetupContext(version string) ([]*PythonContext, error) { jsonschemapythonctx.PackageVersion = "4.17.3" jsonschemapythonctx.PythonVersion = version - // Read the gem meta into + // Read the pypi meta into data, err = os.ReadFile(filepath.Join(jsonschemaMeta, "json")) if err != nil { return nil, err @@ -238,7 +239,7 @@ func SetupContextPreserveURI(version string) ([]*PythonContext, error) { typingextctx.PythonVersion = version typingextctx.PreserveBaseURI = true - // Read the gem meta into + // Read the pypi package meta into data, err := os.ReadFile(filepath.Join(typingextMeta, "json")) if err != nil { return nil, err @@ -291,6 +292,7 @@ func TestGenerateEnvironment(t *testing.T) { "build-base", "busybox", "ca-certificates-bundle", + "py3-supported-pip", "wolfi-base", }, }, @@ -314,6 +316,7 @@ func TestGenerateEnvironment(t *testing.T) { "build-base", "busybox", "ca-certificates-bundle", + "py3-supported-pip", "wolfi-base", }, }, diff --git a/pkg/sca/testdata/generated/x86_64/shbang-test-1-r1.apk b/pkg/sca/testdata/generated/x86_64/shbang-test-1-r1.apk index 76f402ed1ca19b7f146e9e714d6669957f71333d..8c23382b4c30927b7af5973c577dcc1544b97378 100644 GIT binary patch literal 4593 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 H2n6ykRLXtJ delta 4516 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