Skip to content

Commit af6aa0f

Browse files
committed
cmd/go, go/build: add support for binary-only packages
See https://golang.org/design/2775-binary-only-packages for design. Fixes #2775. Change-Id: I33e74eebffadc14d3340bba96083af0dec5172d5 Reviewed-on: https://go-review.googlesource.com/22433 Reviewed-by: Brad Fitzpatrick <[email protected]> Reviewed-by: Ian Lance Taylor <[email protected]>
1 parent 4618dd8 commit af6aa0f

File tree

8 files changed

+177
-22
lines changed

8 files changed

+177
-22
lines changed

src/cmd/go/build.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1323,6 +1323,13 @@ func (b *builder) do(root *action) {
13231323

13241324
// build is the action for building a single package or command.
13251325
func (b *builder) build(a *action) (err error) {
1326+
// Return an error for binary-only package.
1327+
// We only reach this if isStale believes the binary form is
1328+
// either not present or not usable.
1329+
if a.p.BinaryOnly {
1330+
return fmt.Errorf("missing or invalid package binary for binary-only package %s", a.p.ImportPath)
1331+
}
1332+
13261333
// Return an error if the package has CXX files but it's not using
13271334
// cgo nor SWIG, since the CXX files can only be processed by cgo
13281335
// and SWIG.
@@ -1340,6 +1347,7 @@ func (b *builder) build(a *action) (err error) {
13401347
return fmt.Errorf("can't build package %s because it contains Fortran files (%s) but it's not using cgo nor SWIG",
13411348
a.p.ImportPath, strings.Join(a.p.FFiles, ","))
13421349
}
1350+
13431351
defer func() {
13441352
if err != nil && err != errPrintedOutput {
13451353
err = fmt.Errorf("go build %s: %v", a.p.ImportPath, err)

src/cmd/go/go_test.go

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2808,3 +2808,71 @@ func TestFatalInBenchmarkCauseNonZeroExitStatus(t *testing.T) {
28082808
tg.grepBothNot("^ok", "test passed unexpectedly")
28092809
tg.grepBoth("FAIL.*benchfatal", "test did not run everything")
28102810
}
2811+
2812+
func TestBinaryOnlyPackages(t *testing.T) {
2813+
tg := testgo(t)
2814+
defer tg.cleanup()
2815+
tg.makeTempdir()
2816+
tg.setenv("GOPATH", tg.path("."))
2817+
2818+
tg.tempFile("src/p1/p1.go", `//go:binary-only-package
2819+
2820+
package p1
2821+
`)
2822+
tg.wantStale("p1", "cannot access install target", "p1 is binary-only but has no binary, should be stale")
2823+
tg.runFail("install", "p1")
2824+
tg.grepStderr("missing or invalid package binary", "did not report attempt to compile binary-only package")
2825+
2826+
tg.tempFile("src/p1/p1.go", `
2827+
package p1
2828+
import "fmt"
2829+
func F(b bool) { fmt.Printf("hello from p1\n"); if b { F(false) } }
2830+
`)
2831+
tg.run("install", "p1")
2832+
os.Remove(tg.path("src/p1/p1.go"))
2833+
tg.mustNotExist(tg.path("src/p1/p1.go"))
2834+
2835+
tg.tempFile("src/p2/p2.go", `
2836+
package p2
2837+
import "p1"
2838+
func F() { p1.F(true) }
2839+
`)
2840+
tg.runFail("install", "p2")
2841+
tg.grepStderr("no buildable Go source files", "did not complain about missing sources")
2842+
2843+
tg.tempFile("src/p1/missing.go", `//go:binary-only-package
2844+
2845+
package p1
2846+
func G()
2847+
`)
2848+
tg.wantNotStale("p1", "no source code", "should NOT want to rebuild p1 (first)")
2849+
tg.run("install", "-x", "p1") // no-op, up to date
2850+
tg.grepBothNot("/compile", "should not have run compiler")
2851+
tg.run("install", "p2") // does not rebuild p1 (or else p2 will fail)
2852+
tg.wantNotStale("p2", "", "should NOT want to rebuild p2")
2853+
2854+
// changes to the non-source-code do not matter,
2855+
// and only one file needs the special comment.
2856+
tg.tempFile("src/p1/missing2.go", `
2857+
package p1
2858+
func H()
2859+
`)
2860+
tg.wantNotStale("p1", "no source code", "should NOT want to rebuild p1 (second)")
2861+
tg.wantNotStale("p2", "", "should NOT want to rebuild p2")
2862+
2863+
tg.tempFile("src/p3/p3.go", `
2864+
package main
2865+
import (
2866+
"p1"
2867+
"p2"
2868+
)
2869+
func main() {
2870+
p1.F(false)
2871+
p2.F()
2872+
}
2873+
`)
2874+
tg.run("install", "p3")
2875+
2876+
tg.run("run", tg.path("src/p3/p3.go"))
2877+
tg.grepStdout("hello from p1", "did not see message from p1")
2878+
}

src/cmd/go/help.go

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -524,7 +524,15 @@ the extension of the file name. These extensions are:
524524
Files of each of these types except .syso may contain build
525525
constraints, but the go command stops scanning for build constraints
526526
at the first item in the file that is not a blank line or //-style
527-
line comment.
527+
line comment. See the go/build package documentation for
528+
more details.
529+
530+
Non-test Go source files can also include a //go:binary-only-package
531+
comment, indicating that the package sources are included
532+
for documentation only and must not be used to build the
533+
package binary. This enables distribution of Go packages in
534+
their compiled form alone. See the go/build package documentation
535+
for more details.
528536
`,
529537
}
530538

src/cmd/go/list.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ syntax of package template. The default output is equivalent to -f
4343
Stale bool // would 'go install' do anything for this package?
4444
StaleReason string // explanation for Stale==true
4545
Root string // Go root or Go path dir containing this package
46+
ConflictDir string // this directory shadows Dir in $GOPATH
47+
BinaryOnly bool // binary-only package: cannot be recompiled from sources
4648
4749
// Source files
4850
GoFiles []string // .go source files (excluding CgoFiles, TestGoFiles, XTestGoFiles)

src/cmd/go/pkg.go

Lines changed: 30 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ type Package struct {
4242
StaleReason string `json:",omitempty"` // why is Stale true?
4343
Root string `json:",omitempty"` // Go root or Go path dir containing this package
4444
ConflictDir string `json:",omitempty"` // Dir is hidden by this other directory
45+
BinaryOnly bool `json:",omitempty"` // package cannot be recompiled
4546

4647
// Source files
4748
GoFiles []string `json:",omitempty"` // .go source files (excluding CgoFiles, TestGoFiles, XTestGoFiles)
@@ -153,6 +154,8 @@ func (p *Package) copyBuild(pp *build.Package) {
153154
p.Doc = pp.Doc
154155
p.Root = pp.Root
155156
p.ConflictDir = pp.ConflictDir
157+
p.BinaryOnly = pp.BinaryOnly
158+
156159
// TODO? Target
157160
p.Goroot = pp.Goroot
158161
p.Standard = p.Goroot && p.ImportPath != "" && isStandardImportPath(p.ImportPath)
@@ -1046,7 +1049,15 @@ func (p *Package) load(stk *importStack, bp *build.Package, err error) *Package
10461049
}
10471050
}
10481051

1049-
computeBuildID(p)
1052+
if p.BinaryOnly {
1053+
// For binary-only package, use build ID from supplied package binary.
1054+
buildID, err := readBuildID(p)
1055+
if err == nil {
1056+
p.buildID = buildID
1057+
}
1058+
} else {
1059+
computeBuildID(p)
1060+
}
10501061
return p
10511062
}
10521063

@@ -1367,29 +1378,35 @@ func isStale(p *Package) (bool, string) {
13671378
if p.Error != nil {
13681379
return true, "errors loading package"
13691380
}
1381+
if p.Stale {
1382+
return true, p.StaleReason
1383+
}
13701384

1371-
// A package without Go sources means we only found
1372-
// the installed .a file. Since we don't know how to rebuild
1373-
// it, it can't be stale, even if -a is set. This enables binary-only
1374-
// distributions of Go packages, although such binaries are
1375-
// only useful with the specific version of the toolchain that
1376-
// created them.
1377-
if len(p.gofiles) == 0 && !p.usesSwig() {
1378-
return false, "no source files"
1385+
// If this is a package with no source code, it cannot be rebuilt.
1386+
// If the binary is missing, we mark the package stale so that
1387+
// if a rebuild is needed, that rebuild attempt will produce a useful error.
1388+
// (Some commands, such as 'go list', do not attempt to rebuild.)
1389+
if p.BinaryOnly {
1390+
if p.target == "" {
1391+
// Fail if a build is attempted.
1392+
return true, "no source code for package, but no install target"
1393+
}
1394+
if _, err := os.Stat(p.target); err != nil {
1395+
// Fail if a build is attempted.
1396+
return true, "no source code for package, but cannot access install target: " + err.Error()
1397+
}
1398+
return false, "no source code for package"
13791399
}
13801400

13811401
// If the -a flag is given, rebuild everything.
13821402
if buildA {
13831403
return true, "build -a flag in use"
13841404
}
13851405

1386-
// If there's no install target or it's already marked stale, we have to rebuild.
1406+
// If there's no install target, we have to rebuild.
13871407
if p.target == "" {
13881408
return true, "no install target"
13891409
}
1390-
if p.Stale {
1391-
return true, p.StaleReason
1392-
}
13931410

13941411
// Package is stale if completely unbuilt.
13951412
fi, err := os.Stat(p.target)

src/go/build/build.go

Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -308,6 +308,13 @@ const (
308308

309309
// If AllowBinary is set, Import can be satisfied by a compiled
310310
// package object without corresponding sources.
311+
//
312+
// Deprecated:
313+
// The supported way to create a compiled-only package is to
314+
// write source code containing a //go:binary-only-package comment at
315+
// the top of the file. Such a package will be recognized
316+
// regardless of this flag setting (because it has source code)
317+
// and will have BinaryOnly set to true in the returned Package.
311318
AllowBinary
312319

313320
// If ImportComment is set, parse import comments on package statements.
@@ -348,6 +355,7 @@ type Package struct {
348355
PkgObj string // installed .a file
349356
AllTags []string // tags that can influence file selection in this directory
350357
ConflictDir string // this directory shadows Dir in $GOPATH
358+
BinaryOnly bool // cannot be rebuilt from source (has //go:binary-only-package comment)
351359

352360
// Source files
353361
GoFiles []string // .go source files (excluding CgoFiles, TestGoFiles, XTestGoFiles)
@@ -679,7 +687,7 @@ Found:
679687
p.InvalidGoFiles = append(p.InvalidGoFiles, name)
680688
}
681689

682-
match, data, filename, err := ctxt.matchFile(p.Dir, name, true, allTags)
690+
match, data, filename, err := ctxt.matchFile(p.Dir, name, true, allTags, &p.BinaryOnly)
683691
if err != nil {
684692
badFile(err)
685693
continue
@@ -993,7 +1001,7 @@ func parseWord(data []byte) (word, rest []byte) {
9931001
// MatchFile considers the name of the file and may use ctxt.OpenFile to
9941002
// read some or all of the file's content.
9951003
func (ctxt *Context) MatchFile(dir, name string) (match bool, err error) {
996-
match, _, _, err = ctxt.matchFile(dir, name, false, nil)
1004+
match, _, _, err = ctxt.matchFile(dir, name, false, nil, nil)
9971005
return
9981006
}
9991007

@@ -1005,7 +1013,7 @@ func (ctxt *Context) MatchFile(dir, name string) (match bool, err error) {
10051013
// considers text until the first non-comment.
10061014
// If allTags is non-nil, matchFile records any encountered build tag
10071015
// by setting allTags[tag] = true.
1008-
func (ctxt *Context) matchFile(dir, name string, returnImports bool, allTags map[string]bool) (match bool, data []byte, filename string, err error) {
1016+
func (ctxt *Context) matchFile(dir, name string, returnImports bool, allTags map[string]bool, binaryOnly *bool) (match bool, data []byte, filename string, err error) {
10091017
if strings.HasPrefix(name, "_") ||
10101018
strings.HasPrefix(name, ".") {
10111019
return
@@ -1041,7 +1049,11 @@ func (ctxt *Context) matchFile(dir, name string, returnImports bool, allTags map
10411049

10421050
if strings.HasSuffix(filename, ".go") {
10431051
data, err = readImports(f, false, nil)
1052+
if strings.HasSuffix(filename, "_test.go") {
1053+
binaryOnly = nil // ignore //go:binary-only-package comments in _test.go files
1054+
}
10441055
} else {
1056+
binaryOnly = nil // ignore //go:binary-only-package comments in non-Go sources
10451057
data, err = readComments(f)
10461058
}
10471059
f.Close()
@@ -1051,7 +1063,7 @@ func (ctxt *Context) matchFile(dir, name string, returnImports bool, allTags map
10511063
}
10521064

10531065
// Look for +build comments to accept or reject the file.
1054-
if !ctxt.shouldBuild(data, allTags) && !ctxt.UseAllFiles {
1066+
if !ctxt.shouldBuild(data, allTags, binaryOnly) && !ctxt.UseAllFiles {
10551067
return
10561068
}
10571069

@@ -1080,6 +1092,11 @@ func ImportDir(dir string, mode ImportMode) (*Package, error) {
10801092

10811093
var slashslash = []byte("//")
10821094

1095+
// Special comment denoting a binary-only package.
1096+
// See https://golang.org/design/2775-binary-only-packages
1097+
// for more about the design of binary-only packages.
1098+
var binaryOnlyComment = []byte("//go:binary-only-package")
1099+
10831100
// shouldBuild reports whether it is okay to use this file,
10841101
// The rule is that in the file's leading run of // comments
10851102
// and blank lines, which must be followed by a blank line
@@ -1093,7 +1110,13 @@ var slashslash = []byte("//")
10931110
//
10941111
// marks the file as applicable only on Windows and Linux.
10951112
//
1096-
func (ctxt *Context) shouldBuild(content []byte, allTags map[string]bool) bool {
1113+
// If shouldBuild finds a //go:binary-only-package comment in a file that
1114+
// should be built, it sets *binaryOnly to true. Otherwise it does
1115+
// not change *binaryOnly.
1116+
//
1117+
func (ctxt *Context) shouldBuild(content []byte, allTags map[string]bool, binaryOnly *bool) bool {
1118+
sawBinaryOnly := false
1119+
10971120
// Pass 1. Identify leading run of // comments and blank lines,
10981121
// which must be followed by a blank line.
10991122
end := 0
@@ -1128,6 +1151,9 @@ func (ctxt *Context) shouldBuild(content []byte, allTags map[string]bool) bool {
11281151
}
11291152
line = bytes.TrimSpace(line)
11301153
if bytes.HasPrefix(line, slashslash) {
1154+
if bytes.HasPrefix(line, binaryOnlyComment) {
1155+
sawBinaryOnly = true
1156+
}
11311157
line = bytes.TrimSpace(line[len(slashslash):])
11321158
if len(line) > 0 && line[0] == '+' {
11331159
// Looks like a comment +line.
@@ -1147,6 +1173,10 @@ func (ctxt *Context) shouldBuild(content []byte, allTags map[string]bool) bool {
11471173
}
11481174
}
11491175

1176+
if binaryOnly != nil && sawBinaryOnly {
1177+
*binaryOnly = true
1178+
}
1179+
11501180
return allok
11511181
}
11521182

src/go/build/build_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -151,15 +151,15 @@ func TestShouldBuild(t *testing.T) {
151151

152152
ctx := &Context{BuildTags: []string{"tag1"}}
153153
m := map[string]bool{}
154-
if !ctx.shouldBuild([]byte(file1), m) {
154+
if !ctx.shouldBuild([]byte(file1), m, nil) {
155155
t.Errorf("shouldBuild(file1) = false, want true")
156156
}
157157
if !reflect.DeepEqual(m, want1) {
158158
t.Errorf("shouldBuild(file1) tags = %v, want %v", m, want1)
159159
}
160160

161161
m = map[string]bool{}
162-
if ctx.shouldBuild([]byte(file2), m) {
162+
if ctx.shouldBuild([]byte(file2), m, nil) {
163163
t.Errorf("shouldBuild(file2) = true, want false")
164164
}
165165
if !reflect.DeepEqual(m, want2) {
@@ -168,7 +168,7 @@ func TestShouldBuild(t *testing.T) {
168168

169169
m = map[string]bool{}
170170
ctx = &Context{BuildTags: nil}
171-
if !ctx.shouldBuild([]byte(file3), m) {
171+
if !ctx.shouldBuild([]byte(file3), m, nil) {
172172
t.Errorf("shouldBuild(file3) = false, want true")
173173
}
174174
if !reflect.DeepEqual(m, want3) {

src/go/build/doc.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,4 +139,26 @@
139139
// Using GOOS=android matches build tags and files as for GOOS=linux
140140
// in addition to android tags and files.
141141
//
142+
// Binary-Only Packages
143+
//
144+
// It is possible to distribute packages in binary form without including the
145+
// source code used for compiling the package. To do this, the package must
146+
// be distributed with a source file not excluded by build constraints and
147+
// containing a "//go:binary-only-package" comment.
148+
// Like a build constraint, this comment must appear near the top of the file,
149+
// preceded only by blank lines and other line comments and with a blank line
150+
// following the comment, to separate it from the package documentation.
151+
// Unlike build constraints, this comment is only recognized in non-test
152+
// Go source files.
153+
//
154+
// The minimal source code for a binary-only package is therefore:
155+
//
156+
// //go:binary-only-package
157+
//
158+
// package mypkg
159+
//
160+
// The source code may include additional Go code. That code is never compiled
161+
// but will be processed by tools like godoc and might be useful as end-user
162+
// documentation.
163+
//
142164
package build

0 commit comments

Comments
 (0)