Skip to content

Commit a94deb8

Browse files
linzhpaignas
andauthored
build(gazelle): embed Python zip file (#1485)
The runtime dependencies of Gazelle Python extension makes it hard to distribute Gazelle binaries: we have to preserve the runfiles structure and distribute it with Gazelle binaries. Instead, we can build a single Python zip file that comes a built-in interpreter, and embed the zip file into the Go binary in compile time and avoid the runtime dependency. Fixes #1455 --------- Co-authored-by: Ignas Anikevicius <[email protected]>
1 parent 87a9cf1 commit a94deb8

File tree

11 files changed

+90
-55
lines changed

11 files changed

+90
-55
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ A brief description of the categories of changes:
2424
* Make `//python/pip_install:pip_repository_bzl` `bzl_library` target internal
2525
as all of the publicly available symbols (etc. `package_annotation`) are
2626
re-exported via `//python:pip_bzl` `bzl_library`.
27+
* Gazelle Python extension no longer has runtime dependencies. Using `GAZELLE_PYTHON_RUNTIME_DEPS` from `@rules_python_gazelle_plugin//:def.bzl` is no longer necessary.
2728

2829
### Fixed
2930

examples/build_file_generation/BUILD.bazel

-2
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ load("@bazel_gazelle//:def.bzl", "gazelle")
66
load("@pip//:requirements.bzl", "all_whl_requirements")
77
load("@rules_python//python:defs.bzl", "py_binary", "py_library", "py_test")
88
load("@rules_python//python:pip.bzl", "compile_pip_requirements")
9-
load("@rules_python_gazelle_plugin//:def.bzl", "GAZELLE_PYTHON_RUNTIME_DEPS")
109
load("@rules_python_gazelle_plugin//manifest:defs.bzl", "gazelle_python_manifest")
1110
load("@rules_python_gazelle_plugin//modules_mapping:def.bzl", "modules_mapping")
1211

@@ -56,7 +55,6 @@ gazelle_python_manifest(
5655
# See https://github.com/bazelbuild/bazel-gazelle/blob/master/extend.rst#example
5756
gazelle(
5857
name = "gazelle",
59-
data = GAZELLE_PYTHON_RUNTIME_DEPS,
6058
gazelle = "@rules_python_gazelle_plugin//python:gazelle_binary",
6159
)
6260

examples/bzlmod_build_file_generation/BUILD.bazel

-2
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ load("@bazel_gazelle//:def.bzl", "gazelle")
99
load("@pip//:requirements.bzl", "all_whl_requirements")
1010
load("@rules_python//python:defs.bzl", "py_binary", "py_library", "py_test")
1111
load("@rules_python//python:pip.bzl", "compile_pip_requirements")
12-
load("@rules_python_gazelle_plugin//:def.bzl", "GAZELLE_PYTHON_RUNTIME_DEPS")
1312
load("@rules_python_gazelle_plugin//manifest:defs.bzl", "gazelle_python_manifest")
1413
load("@rules_python_gazelle_plugin//modules_mapping:def.bzl", "modules_mapping")
1514

@@ -70,7 +69,6 @@ gazelle_python_manifest(
7069
# See: https://github.com/bazelbuild/bazel-gazelle#fix-and-update
7170
gazelle(
7271
name = "gazelle",
73-
data = GAZELLE_PYTHON_RUNTIME_DEPS,
7472
gazelle = "@rules_python_gazelle_plugin//python:gazelle_binary",
7573
)
7674

gazelle/README.md

+3-3
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@ Gazelle may be run by Bazel using the gazelle rule, or it may be installed and r
77

88
This directory contains a plugin for
99
[Gazelle](https://github.com/bazelbuild/bazel-gazelle)
10-
that generates BUILD files content for Python code.
10+
that generates BUILD files content for Python code. When Gazelle is run as a command line tool with this plugin, it embeds a Python interpreter resolved during the plugin build.
11+
The behavior of the plugin is slightly different with different version of the interpreter as the Python `stdlib` changes with every minor version release.
12+
Distributors of Gazelle binaries should, therefore, build a Gazelle binary for each OS+CPU architecture+Minor Python version combination they are targeting.
1113

1214
The following instructions are for when you use [bzlmod](https://docs.bazel.build/versions/5.0.0/bzlmod.html).
1315
Please refer to older documentation that includes instructions on how to use Gazelle
@@ -125,7 +127,6 @@ with the rules_python extension included. This typically goes in your root
125127

126128
```starlark
127129
load("@bazel_gazelle//:def.bzl", "gazelle")
128-
load("@rules_python_gazelle_plugin//:def.bzl", "GAZELLE_PYTHON_RUNTIME_DEPS")
129130

130131
# Our gazelle target points to the python gazelle binary.
131132
# This is the simple case where we only need one language supported.
@@ -134,7 +135,6 @@ load("@rules_python_gazelle_plugin//:def.bzl", "GAZELLE_PYTHON_RUNTIME_DEPS")
134135
# See https://github.com/bazelbuild/bazel-gazelle/blob/master/extend.rst#example
135136
gazelle(
136137
name = "gazelle",
137-
data = GAZELLE_PYTHON_RUNTIME_DEPS,
138138
gazelle = "@rules_python_gazelle_plugin//python:gazelle_binary",
139139
)
140140
```

gazelle/def.bzl

-2
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,4 @@
1616
"""
1717

1818
GAZELLE_PYTHON_RUNTIME_DEPS = [
19-
"@rules_python_gazelle_plugin//python:parse",
20-
"@rules_python_gazelle_plugin//python:std_modules",
2119
]

gazelle/python/BUILD.bazel

+14-13
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,7 @@ go_library(
1616
"std_modules.go",
1717
"target.go",
1818
],
19-
data = [
20-
":parse",
21-
":std_modules",
22-
],
19+
embedsrcs = [":helper.zip"],
2320
importpath = "github.com/bazelbuild/rules_python/gazelle/python",
2421
visibility = ["//visibility:public"],
2522
deps = [
@@ -36,33 +33,37 @@ go_library(
3633
"@com_github_emirpasic_gods//lists/singlylinkedlist",
3734
"@com_github_emirpasic_gods//sets/treeset",
3835
"@com_github_emirpasic_gods//utils",
39-
"@io_bazel_rules_go//go/runfiles",
4036
],
4137
)
4238

4339
py_binary(
44-
name = "parse",
45-
srcs = ["parse.py"],
40+
name = "helper",
41+
srcs = [
42+
"__main__.py",
43+
"parse.py",
44+
"std_modules.py",
45+
],
46+
main = "__main__.py",
4647
visibility = ["//visibility:public"],
4748
)
4849

49-
py_binary(
50-
name = "std_modules",
51-
srcs = ["std_modules.py"],
52-
visibility = ["//visibility:public"],
50+
filegroup(
51+
name = "helper.zip",
52+
srcs = [":helper"],
53+
output_group = "python_zip_file",
5354
)
5455

5556
go_test(
5657
name = "python_test",
5758
srcs = ["python_test.go"],
5859
data = [
5960
":gazelle_binary",
60-
":parse",
61-
":std_modules",
61+
":helper",
6262
] + glob(["testdata/**"]),
6363
deps = [
6464
"@bazel_gazelle//testtools:go_default_library",
6565
"@com_github_ghodss_yaml//:yaml",
66+
"@io_bazel_rules_go//go/runfiles:go_default_library",
6667
"@io_bazel_rules_go//go/tools/bazel:go_default_library",
6768
],
6869
)

gazelle/python/__main__.py

+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# Copyright 2023 The Bazel Authors. All rights reserved.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
# parse.py is a long-living program that communicates over STDIN and STDOUT.
16+
# STDIN receives parse requests, one per line. It outputs the parsed modules and
17+
# comments from all the files from each request.
18+
19+
import parse
20+
import std_modules
21+
import sys
22+
23+
if __name__ == "__main__":
24+
if len(sys.argv) < 2:
25+
sys.exit("Please provide subcommand, either print or std_modules")
26+
if sys.argv[1] == "parse":
27+
sys.exit(parse.main(sys.stdin, sys.stdout))
28+
elif sys.argv[1] == "std_modules":
29+
sys.exit(std_modules.main(sys.stdin, sys.stdout))
30+
else:
31+
sys.exit("Unknown subcommand: " + sys.argv[1])

gazelle/python/lifecycle.go

+26
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,37 @@ package python
1616

1717
import (
1818
"context"
19+
_ "embed"
1920
"github.com/bazelbuild/bazel-gazelle/language"
21+
"log"
22+
"os"
23+
)
24+
25+
var (
26+
//go:embed helper.zip
27+
helperZip []byte
28+
helperPath string
2029
)
2130

2231
type LifeCycleManager struct {
2332
language.BaseLifecycleManager
33+
pyzFilePath string
2434
}
2535

2636
func (l *LifeCycleManager) Before(ctx context.Context) {
37+
helperPath = os.Getenv("GAZELLE_PYTHON_HELPER")
38+
if helperPath == "" {
39+
pyzFile, err := os.CreateTemp("", "python_zip_")
40+
if err != nil {
41+
log.Fatalf("failed to write parser zip: %v", err)
42+
}
43+
defer pyzFile.Close()
44+
helperPath = pyzFile.Name()
45+
l.pyzFilePath = helperPath
46+
if _, err := pyzFile.Write(helperZip); err != nil {
47+
log.Fatalf("cannot write %q: %v", helperPath, err)
48+
}
49+
}
2750
startParserProcess(ctx)
2851
startStdModuleProcess(ctx)
2952
}
@@ -34,4 +57,7 @@ func (l *LifeCycleManager) DoneGeneratingRules() {
3457

3558
func (l *LifeCycleManager) AfterResolvingDeps(ctx context.Context) {
3659
shutdownStdModuleProcess()
60+
if l.pyzFilePath != "" {
61+
os.Remove(l.pyzFilePath)
62+
}
3763
}

gazelle/python/parser.go

+4-16
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ package python
1717
import (
1818
"bufio"
1919
"context"
20+
_ "embed"
2021
"encoding/json"
2122
"fmt"
2223
"io"
@@ -26,7 +27,6 @@ import (
2627
"strings"
2728
"sync"
2829

29-
"github.com/bazelbuild/rules_go/go/runfiles"
3030
"github.com/emirpasic/gods/sets/treeset"
3131
godsutils "github.com/emirpasic/gods/utils"
3232
)
@@ -38,21 +38,9 @@ var (
3838
)
3939

4040
func startParserProcess(ctx context.Context) {
41-
rfiles, err := runfiles.New()
42-
if err != nil {
43-
log.Printf("failed to create a runfiles object: %v\n", err)
44-
os.Exit(1)
45-
}
46-
47-
parseScriptRunfile, err := rfiles.Rlocation("rules_python_gazelle_plugin/python/parse")
48-
if err != nil {
49-
log.Printf("failed to initialize parser: %v\n", err)
50-
os.Exit(1)
51-
}
52-
53-
cmd := exec.CommandContext(ctx, parseScriptRunfile)
54-
cmd.Env = append(os.Environ(), rfiles.Env()...)
55-
41+
// due to #691, we need a system interpreter to boostrap, part of which is
42+
// to locate the hermetic interpreter.
43+
cmd := exec.CommandContext(ctx, "python3", helperPath, "parse")
5644
cmd.Stderr = os.Stderr
5745

5846
stdin, err := cmd.StdinPipe()

gazelle/python/python_test.go

+6
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import (
3131
"time"
3232

3333
"github.com/bazelbuild/bazel-gazelle/testtools"
34+
"github.com/bazelbuild/rules_go/go/runfiles"
3435
"github.com/bazelbuild/rules_go/go/tools/bazel"
3536
"github.com/ghodss/yaml"
3637
)
@@ -159,6 +160,11 @@ func testPath(t *testing.T, name string, files []bazel.RunfileEntry) {
159160
cmd.Stdout = &stdout
160161
cmd.Stderr = &stderr
161162
cmd.Dir = workspaceRoot
163+
helperScript, err := runfiles.Rlocation("rules_python_gazelle_plugin/python/helper")
164+
if err != nil {
165+
t.Fatalf("failed to initialize Python heler: %v", err)
166+
}
167+
cmd.Env = append(os.Environ(), "GAZELLE_PYTHON_HELPER="+helperScript)
162168
if err := cmd.Run(); err != nil {
163169
var e *exec.ExitError
164170
if !errors.As(err, &e) {

gazelle/python/std_modules.go

+5-17
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ package python
1717
import (
1818
"bufio"
1919
"context"
20+
_ "embed"
2021
"fmt"
2122
"io"
2223
"log"
@@ -25,8 +26,6 @@ import (
2526
"strconv"
2627
"strings"
2728
"sync"
28-
29-
"github.com/bazelbuild/rules_go/go/runfiles"
3029
)
3130

3231
var (
@@ -39,23 +38,12 @@ var (
3938
func startStdModuleProcess(ctx context.Context) {
4039
stdModulesSeen = make(map[string]struct{})
4140

42-
rfiles, err := runfiles.New()
43-
if err != nil {
44-
log.Printf("failed to create a runfiles object: %v\n", err)
45-
os.Exit(1)
46-
}
47-
48-
stdModulesScriptRunfile, err := rfiles.Rlocation("rules_python_gazelle_plugin/python/std_modules")
49-
if err != nil {
50-
log.Printf("failed to initialize std_modules: %v\n", err)
51-
os.Exit(1)
52-
}
53-
54-
cmd := exec.CommandContext(ctx, stdModulesScriptRunfile)
55-
41+
// due to #691, we need a system interpreter to boostrap, part of which is
42+
// to locate the hermetic interpreter.
43+
cmd := exec.CommandContext(ctx, "python3", helperPath, "std_modules")
5644
cmd.Stderr = os.Stderr
5745
// All userland site-packages should be ignored.
58-
cmd.Env = append([]string{"PYTHONNOUSERSITE=1"}, rfiles.Env()...)
46+
cmd.Env = []string{"PYTHONNOUSERSITE=1"}
5947

6048
stdin, err := cmd.StdinPipe()
6149
if err != nil {

0 commit comments

Comments
 (0)