-
Notifications
You must be signed in to change notification settings - Fork 19
projects: Correctly build multi-arch Python functions #192
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
0a934c5
3a98cc4
a3522cb
f059e20
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -19,10 +19,12 @@ package functions | |||||
| import ( | ||||||
| "bytes" | ||||||
| "context" | ||||||
| "fmt" | ||||||
| "io" | ||||||
| "net/http" | ||||||
| "path" | ||||||
| "path/filepath" | ||||||
| "strings" | ||||||
|
|
||||||
| "github.com/google/go-containerregistry/pkg/name" | ||||||
| v1 "github.com/google/go-containerregistry/pkg/v1" | ||||||
|
|
@@ -42,15 +44,16 @@ import ( | |||||
| ) | ||||||
|
|
||||||
| const ( | ||||||
| // pythonBuildImage is the image in which we build the function. Its python | ||||||
| // version must match the python version of pythonRuntimeImage. | ||||||
| // pythonVersion is the Python version we use. Both the build image and the | ||||||
| // runtime image must have this version of Python installed. | ||||||
| pythonVersion = "3.13" | ||||||
|
|
||||||
| // pythonBuildImage is the image in which we build the function. | ||||||
| pythonBuildImage = "docker.io/library/debian:13-slim" | ||||||
| // pythonRuntimeImage is the distroless base used at runtime. | ||||||
| pythonRuntimeImage = "gcr.io/distroless/python3-debian13:nonroot" | ||||||
| // pythonBuildScript is the shell pipeline that runs in the build | ||||||
| // container. Mirrors function-template-python's Dockerfile: install hatch | ||||||
| // in a throwaway venv, build a wheel, install the wheel into a fresh venv | ||||||
| // at /fn. | ||||||
| // container. | ||||||
| // | ||||||
| // TODO(adamwg): We should build an image with python3 and python3-venv | ||||||
| // pre-installed so we don't have to install them for every build. | ||||||
|
|
@@ -61,8 +64,18 @@ apt-get install -y --no-install-recommends python3 python3-venv | |||||
| python3 -m venv /build | ||||||
| /build/bin/pip install --quiet hatch | ||||||
| /build/bin/hatch build -t wheel /whl | ||||||
| python3 -m venv /fn | ||||||
| /fn/bin/pip install --quiet /whl/*.whl | ||||||
| for arch in $ARCHS ; do | ||||||
| python3 -m venv /fn_$arch | ||||||
| /fn_$arch/bin/pip install \ | ||||||
| --platform=manylinux2014_$arch \ | ||||||
| --platform=manylinux_2_28_$arch \ | ||||||
| --platform=manylinux_2_34_$arch \ | ||||||
| --platform=manylinux_2_39_$arch \ | ||||||
| --platform=manylinux_1_2_$arch \ | ||||||
| --only-binary=:all: \ | ||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. could this cause issues if a function author wants a dep that doesn't ship a 2014 wheel? they're getting a little long in the tooth. and according to my 🤖 the failure mode is a hard error that's not super informative
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. My understanding of the Python ecosystem is limited, but this makes sense to me. Based on the manylinux README, I've added |
||||||
| --target=/fn_$arch/lib/python$PY_VERSION/site-packages \ | ||||||
| /whl/*.whl | ||||||
| done | ||||||
| ` | ||||||
| ) | ||||||
|
|
||||||
|
|
@@ -101,7 +114,7 @@ func (b *pythonBuilder) Build(ctx context.Context, c BuildContext) ([]v1.Image, | |||||
| return nil, errors.Wrap(err, "python builds require a Docker-compatible container runtime") | ||||||
| } | ||||||
|
|
||||||
| venvTar, err := b.buildVenv(ctx, c) | ||||||
| venvTars, err := b.buildVenv(ctx, c) | ||||||
| if err != nil { | ||||||
| return nil, err | ||||||
| } | ||||||
|
|
@@ -130,7 +143,7 @@ func (b *pythonBuilder) Build(ctx context.Context, c BuildContext) ([]v1.Image, | |||||
| } | ||||||
|
|
||||||
| venvLayer, err := tarball.LayerFromOpener(func() (io.ReadCloser, error) { | ||||||
| return io.NopCloser(bytes.NewReader(venvTar)), nil | ||||||
| return io.NopCloser(bytes.NewReader(venvTars[arch])), nil | ||||||
| }) | ||||||
| if err != nil { | ||||||
| return errors.Wrap(err, "failed to create venv layer") | ||||||
|
|
@@ -141,7 +154,7 @@ func (b *pythonBuilder) Build(ctx context.Context, c BuildContext) ([]v1.Image, | |||||
| return errors.Wrap(err, "failed to append venv layer") | ||||||
| } | ||||||
|
|
||||||
| img, err = configurePythonImage(img) | ||||||
| img, err = configurePythonImage(img, arch) | ||||||
| if err != nil { | ||||||
| return errors.Wrap(err, "failed to configure python image") | ||||||
| } | ||||||
|
|
@@ -154,14 +167,13 @@ func (b *pythonBuilder) Build(ctx context.Context, c BuildContext) ([]v1.Image, | |||||
| return images, eg.Wait() | ||||||
| } | ||||||
|
|
||||||
| // buildVenv runs the build container against the function source and returns a | ||||||
| // tar of /fn suitable for use as an image layer (entries are rooted at | ||||||
| // /fn/...). | ||||||
| // buildVenv runs the build container against the function source and returns | ||||||
| // tars of /fn_<arch> for each architecture, suitable for use as an image layer. | ||||||
| // | ||||||
| // The function source is staged at /<FunctionPath> and, if a python schemas | ||||||
| // tree exists, /<SchemasPath>/python/ — preserving the project's relative | ||||||
| // layout so that pip resolves the schemas path-dep from pyproject.toml. | ||||||
| func (b *pythonBuilder) buildVenv(ctx context.Context, c BuildContext) ([]byte, error) { | ||||||
| func (b *pythonBuilder) buildVenv(ctx context.Context, c BuildContext) (map[string][]byte, error) { | ||||||
| fnFS := c.FunctionFS() | ||||||
| // Exclude any venv the user might have created in the function directory | ||||||
| // for local development, since (a) we don't need it, and (b) it will | ||||||
|
|
@@ -191,8 +203,20 @@ func (b *pythonBuilder) buildVenv(ctx context.Context, c BuildContext) ([]byte, | |||||
| buildImage = rewritten | ||||||
| } | ||||||
|
|
||||||
| pyArchitectures := make([]string, len(c.Architectures)) | ||||||
| for i, a := range c.Architectures { | ||||||
| pyArchitectures[i], err = pythonArchitecture(a) | ||||||
| if err != nil { | ||||||
| return nil, err | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
| opts := []docker.StartContainerOption{ | ||||||
| docker.StartWithCopyFiles(fnTar, "/"), | ||||||
| docker.StartWithEnv( | ||||||
| "ARCHS="+strings.Join(pyArchitectures, " "), | ||||||
| "PY_VERSION="+pythonVersion, | ||||||
| ), | ||||||
| docker.StartWithCommand([]string{"sh", "-c", pythonBuildScript}), | ||||||
| docker.StartWithWorkingDirectory("/" + filepath.ToSlash(c.FunctionPath)), | ||||||
| } | ||||||
|
|
@@ -212,20 +236,44 @@ func (b *pythonBuilder) buildVenv(ctx context.Context, c BuildContext) ([]byte, | |||||
| return nil, errors.Wrap(err, "python build container failed") | ||||||
| } | ||||||
|
|
||||||
| return docker.TarFromContainer(ctx, cid, "/fn") | ||||||
| ret := make(map[string][]byte) | ||||||
| for _, arch := range c.Architectures { | ||||||
| pyArch, _ := pythonArchitecture(arch) // Ignore the error since we already did this once. | ||||||
| ret[arch], err = docker.TarFromContainer(ctx, cid, fmt.Sprintf("/fn_%s", pyArch)) | ||||||
| if err != nil { | ||||||
| return nil, errors.Wrapf(err, "failed to retrieve built function for architecture %s", arch) | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
| return ret, nil | ||||||
| } | ||||||
|
|
||||||
| func pythonArchitecture(a string) (string, error) { | ||||||
| switch a { | ||||||
| case "amd64": | ||||||
| return "x86_64", nil | ||||||
| case "arm64": | ||||||
| return "aarch64", nil | ||||||
| default: | ||||||
| return "", errors.Errorf("unable to determine python architecture for architecture %s", a) | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win Error message could mention supported architectures.
💚 Suggested improvement-default:
- return "", errors.Errorf("unable to determine python architecture for architecture %s", a)
+default:
+ return "", errors.Errorf("unsupported architecture %s for Python functions: only amd64 and arm64 are supported", a)📝 Committable suggestion
Suggested change
🤖 Prompt for AI AgentsSource: Path instructions |
||||||
| } | ||||||
| } | ||||||
|
|
||||||
| // configurePythonImage sets the runtime configuration on the final image to | ||||||
| // match function-template-python: nonroot user, the function entrypoint, and | ||||||
| // the gRPC port. | ||||||
| func configurePythonImage(img v1.Image) (v1.Image, error) { | ||||||
| func configurePythonImage(img v1.Image, arch string) (v1.Image, error) { | ||||||
| cfgFile, err := img.ConfigFile() | ||||||
| if err != nil { | ||||||
| return nil, errors.Wrap(err, "failed to get config file") | ||||||
| } | ||||||
| cfg := cfgFile.Config | ||||||
|
|
||||||
| cfg.Entrypoint = []string{"/fn/bin/function"} | ||||||
| pyArch, err := pythonArchitecture(arch) | ||||||
| if err != nil { | ||||||
| return nil, err | ||||||
| } | ||||||
| cfg.Entrypoint = []string{fmt.Sprintf("/fn_%s/lib/python%s/site-packages/bin/function", pyArch, pythonVersion)} | ||||||
| cfg.Cmd = nil | ||||||
| cfg.WorkingDir = "/" | ||||||
| cfg.User = "nonroot:nonroot" | ||||||
|
|
||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
Repository: crossplane/cli
Length of output: 722
🏁 Script executed:
Repository: crossplane/cli
Length of output: 4276
🏁 Script executed:
Repository: crossplane/cli
Length of output: 884
🌐 Web query:
nixpkgs 1766437c5509f444c1b15331e82b8b6a9b967000 golangci-lint package version💡 Result:
At commit 1766437c5509f444c1b15331e82b8b6a9b967000, which corresponds to the release-25.11 branch of nixpkgs [1][2], the golangci-lint package was at version 2.6.2 [1]. This version of the package was configured to be built using buildGo125Module [1]. As with other versions of golangci-lint in nixpkgs, this package is explicitly tied to a specific Go version to ensure compatibility, as the tool has historically required code changes to support new Go releases [3][1][4].
Citations:
Use
linters.exclusions.rulesfor golangci-lint v2.golangci.yml:123is pinned throughpkgs.unstable.golangci-lint2.6.2, sogoconst.ignore-testsno longer matches this version. Can you move the test-file exclusion tolinters.exclusions.rulesso it keeps working?🤖 Prompt for AI Agents
Source: MCP tools