From d9ef79ef1c1c1e5b4c5cb37c7a2c6215573c798a Mon Sep 17 00:00:00 2001 From: Adam Wolfe Gordon Date: Fri, 10 Jul 2026 16:47:55 -0600 Subject: [PATCH 1/4] lint: Have the goconst linter ignore tests goconst detects duplicated constants and forces us to declare them as consts. This is unhelpful when the majority of occurences are in test fixtures. Signed-off-by: Adam Wolfe Gordon (cherry picked from commit 0a934c5c8bf7f881cb0ca60f616406cce13c065f) --- .golangci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.golangci.yml b/.golangci.yml index 91563e48..eed2b8f3 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -120,6 +120,7 @@ linters: goconst: min-len: 3 min-occurrences: 5 + ignore-tests: true gocritic: enabled-tags: - performance From cb287ce8fc2d7664208cd1992416814ed0ffe5be Mon Sep 17 00:00:00 2001 From: Adam Wolfe Gordon Date: Fri, 10 Jul 2026 16:48:46 -0600 Subject: [PATCH 2/4] projects: Correctly build multi-arch Python functions Even though Python is interpreted, Python function images include some platform-specific shared libraries. Previously, we were building only for the host architecture (e.g., arm64 on macOS), which meant we ended up with incompatible shared libraries in the resulting function image. Update the Python SDK builder to produce a layer per architecture, each containing the correct shared libraries for that architecture. Note that the builder image is always the host architecture - we don't want to assume the user has qemu or any other way to run non-native containers. Signed-off-by: Adam Wolfe Gordon (cherry picked from commit 3a98cc4784e03a0c82d68af2e9cbe66ba20361dc) --- internal/project/functions/python.go | 67 +++++++++++++++++++++------- 1 file changed, 52 insertions(+), 15 deletions(-) diff --git a/internal/project/functions/python.go b/internal/project/functions/python.go index 78a62209..7957f194 100644 --- a/internal/project/functions/python.go +++ b/internal/project/functions/python.go @@ -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" @@ -48,9 +50,7 @@ const ( // 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 +61,13 @@ 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 \ + --only-binary=:all: \ + --target=/fn_$arch/lib/python3.13/site-packages \ + /whl/*.whl +done ` ) @@ -101,7 +106,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 +135,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 +146,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 +159,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_ for each architecture, suitable for use as an image layer. // // The function source is staged at / and, if a python schemas // tree exists, //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 +195,17 @@ 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, " ")), docker.StartWithCommand([]string{"sh", "-c", pythonBuildScript}), docker.StartWithWorkingDirectory("/" + filepath.ToSlash(c.FunctionPath)), } @@ -212,20 +225,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) + } } // 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/python3.13/site-packages/bin/function", pyArch)} cfg.Cmd = nil cfg.WorkingDir = "/" cfg.User = "nonroot:nonroot" From b098d40b6dba3e236b0f7eef49bdbcea855ae1ac Mon Sep 17 00:00:00 2001 From: Adam Wolfe Gordon Date: Tue, 14 Jul 2026 10:59:09 -0600 Subject: [PATCH 3/4] projects: Allow Python dependencies to use any manylinux version Signed-off-by: Adam Wolfe Gordon (cherry picked from commit a3522cb7255d0e1477d718d39b1fc4f50fa73b2e) --- internal/project/functions/python.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/internal/project/functions/python.go b/internal/project/functions/python.go index 7957f194..84d76029 100644 --- a/internal/project/functions/python.go +++ b/internal/project/functions/python.go @@ -63,7 +63,12 @@ python3 -m venv /build /build/bin/hatch build -t wheel /whl for arch in $ARCHS ; do python3 -m venv /fn_$arch - /fn_$arch/bin/pip install --platform=manylinux2014_$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: \ --target=/fn_$arch/lib/python3.13/site-packages \ /whl/*.whl From 7be3c8e3e22922a6389e23d7c4625f171783e9c9 Mon Sep 17 00:00:00 2001 From: Adam Wolfe Gordon Date: Tue, 14 Jul 2026 11:18:07 -0600 Subject: [PATCH 4/4] projects: Explicitly declare the Python version we use Signed-off-by: Adam Wolfe Gordon (cherry picked from commit f059e202d58e8658cce755efeb81ee75791fedc1) --- internal/project/functions/python.go | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/internal/project/functions/python.go b/internal/project/functions/python.go index 84d76029..55fd0f38 100644 --- a/internal/project/functions/python.go +++ b/internal/project/functions/python.go @@ -44,8 +44,11 @@ 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" @@ -70,7 +73,7 @@ for arch in $ARCHS ; do --platform=manylinux_2_39_$arch \ --platform=manylinux_1_2_$arch \ --only-binary=:all: \ - --target=/fn_$arch/lib/python3.13/site-packages \ + --target=/fn_$arch/lib/python$PY_VERSION/site-packages \ /whl/*.whl done ` @@ -210,7 +213,10 @@ func (b *pythonBuilder) buildVenv(ctx context.Context, c BuildContext) (map[stri opts := []docker.StartContainerOption{ docker.StartWithCopyFiles(fnTar, "/"), - docker.StartWithEnv("ARCHS=" + strings.Join(pyArchitectures, " ")), + docker.StartWithEnv( + "ARCHS="+strings.Join(pyArchitectures, " "), + "PY_VERSION="+pythonVersion, + ), docker.StartWithCommand([]string{"sh", "-c", pythonBuildScript}), docker.StartWithWorkingDirectory("/" + filepath.ToSlash(c.FunctionPath)), } @@ -267,7 +273,7 @@ func configurePythonImage(img v1.Image, arch string) (v1.Image, error) { if err != nil { return nil, err } - cfg.Entrypoint = []string{fmt.Sprintf("/fn_%s/lib/python3.13/site-packages/bin/function", pyArch)} + 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"