diff --git a/src/cmd/go/internal/modindex/build.go b/src/cmd/go/internal/modindex/build.go index c0acb8ff587d7f..ff7dddcb73392a 100644 --- a/src/cmd/go/internal/modindex/build.go +++ b/src/cmd/go/internal/modindex/build.go @@ -274,8 +274,14 @@ func getFileInfo(dir, name string, fset *token.FileSet) (*fileInfo, error) { return nil, fmt.Errorf("read %s: %v", info.name, err) } - // Look for +build comments to accept or reject the file. - info.goBuildConstraint, info.plusBuildConstraints, info.binaryOnly, err = getConstraints(info.header) + // Look for go:build comments to accept or reject the file. + // For non-Go source files, also recognise language-specific comment prefixes + // (e.g. "% go:build" for MATLAB .m files). + var commentPrefix []byte + if !strings.HasSuffix(name, ".go") { + commentPrefix = extCommentPrefix[ext] + } + info.goBuildConstraint, info.plusBuildConstraints, info.binaryOnly, err = getConstraintsWithPrefix(info.header, commentPrefix) if err != nil { return nil, fmt.Errorf("%s: %v", name, err) } @@ -302,12 +308,23 @@ var ( errMultipleGoBuild = errors.New("multiple //go:build comments") ) +// extCommentPrefix maps non-Go file extensions to their line-comment prefix +// for go:build constraints. See go/build.extCommentPrefix for the full rationale. +var extCommentPrefix = map[string][]byte{ + ".m": []byte("% go:build"), + ".f90": []byte("! go:build"), +} + func isGoBuildComment(line []byte) bool { - if !bytes.HasPrefix(line, goBuildComment) { + return isGoBuildCommentWithPrefix(line, goBuildComment) +} + +func isGoBuildCommentWithPrefix(line, prefix []byte) bool { + if !bytes.HasPrefix(line, prefix) { return false } line = bytes.TrimSpace(line) - rest := line[len(goBuildComment):] + rest := line[len(prefix):] return len(rest) == 0 || len(bytes.TrimSpace(rest)) < len(rest) } @@ -317,17 +334,39 @@ func isGoBuildComment(line []byte) bool { var binaryOnlyComment = []byte("//go:binary-only-package") func getConstraints(content []byte) (goBuild string, plusBuild []string, binaryOnly bool, err error) { - // Identify leading run of // comments and blank lines, - // which must be followed by a blank line. - // Also identify any //go:build comments. - content, goBuildBytes, sawBinaryOnly, err := parseFileHeader(content) + return getConstraintsWithPrefix(content, nil) +} + +// getConstraintsWithPrefix is like getConstraints but accepts an explicit +// line-comment prefix for non-Go source files (e.g. "% go:build" for MATLAB). +// When commentPrefix is nil the function behaves exactly like getConstraints. +func getConstraintsWithPrefix(content []byte, commentPrefix []byte) (goBuild string, plusBuild []string, binaryOnly bool, err error) { + var goBuildBytes []byte + var sawBinaryOnly bool + if commentPrefix == nil { + content, goBuildBytes, sawBinaryOnly, err = parseFileHeader(content) + } else { + content, goBuildBytes, sawBinaryOnly, err = parseFileHeaderWithPrefix(content, commentPrefix) + } if err != nil { return "", nil, false, err } - // If //go:build line is present, it controls, so no need to look for +build . - // Otherwise, get plusBuild constraints. - if goBuildBytes == nil { + // If a go:build line is present it controls; no need to look for +build. + // For non-Go files the legacy +build form is not supported. + if goBuildBytes != nil { + // Normalise: strip the language-specific prefix and re-wrap as + // "//go:build " so the rest of the toolchain can parse it. + prefix := goBuildComment + if commentPrefix != nil { + prefix = commentPrefix + } + expr := string(bytes.TrimSpace(goBuildBytes[len(prefix):])) + return "//go:build " + expr, nil, sawBinaryOnly, nil + } + + if commentPrefix == nil { + // Legacy // +build processing only for Go-style comments. p := content for len(p) > 0 { line := p @@ -348,7 +387,7 @@ func getConstraints(content []byte) (goBuild string, plusBuild []string, binaryO } } - return string(goBuildBytes), plusBuild, sawBinaryOnly, nil + return "", plusBuild, sawBinaryOnly, nil } func parseFileHeader(content []byte) (trimmed, goBuild []byte, sawBinaryOnly bool, err error) { @@ -418,6 +457,44 @@ Lines: return content[:end], goBuild, sawBinaryOnly, nil } +// parseFileHeaderWithPrefix is the non-Go analogue of parseFileHeader. +// It scans content for a build constraint line introduced by commentPrefix +// (e.g. []byte("% go:build") for MATLAB .m files). +// Block-comment tracking is omitted because non-Go languages targeted here +// do not use /* */ block comments in the same way. +func parseFileHeaderWithPrefix(content []byte, commentPrefix []byte) (trimmed, goBuild []byte, sawBinaryOnly bool, err error) { + end := 0 + p := content + ended := false + +Lines: + for len(p) > 0 { + line := p + if i := bytes.IndexByte(line, '\n'); i >= 0 { + line, p = line[:i], p[i+1:] + } else { + p = p[len(p):] + } + line = bytes.TrimSpace(line) + if len(line) == 0 && !ended { + end = len(content) - len(p) + continue Lines + } + if !bytes.HasPrefix(line, commentPrefix[:1]) { + ended = true + continue Lines + } + if isGoBuildCommentWithPrefix(line, commentPrefix) { + if goBuild != nil { + return nil, nil, false, errMultipleGoBuild + } + goBuild = line + } + } + + return content[:end], goBuild, false, nil +} + // saveCgo saves the information from the #cgo lines in the import "C" comment. // These lines set CFLAGS, CPPFLAGS, CXXFLAGS and LDFLAGS and pkg-config directives // that affect the way cgo's C code is built. diff --git a/src/go/build/build.go b/src/go/build/build.go index 4c6be413abd237..8c976402faeb53 100644 --- a/src/go/build/build.go +++ b/src/go/build/build.go @@ -1494,7 +1494,15 @@ func (ctxt *Context) matchFile(dir, name string, allTags map[string]bool, binary } // Look for go:build comments to accept or reject the file. - ok, sawBinaryOnly, err := ctxt.shouldBuild(info.header, allTags) + // For non-Go source files we also recognise language-specific comment + // prefixes (e.g. "% go:build" for MATLAB .m files) so that those files + // can carry build constraints without the //go:build syntax, which is + // invalid in their respective languages. + var commentPrefix []byte + if !strings.HasSuffix(name, ".go") { + commentPrefix = goBuildCommentForExt(ext) + } + ok, sawBinaryOnly, err := ctxt.shouldBuildWithPrefix(info.header, allTags, commentPrefix) if err != nil { return nil, fmt.Errorf("%s: %v", name, err) } @@ -1536,12 +1544,60 @@ var ( errMultipleGoBuild = errors.New("multiple //go:build comments") ) +// extCommentPrefix maps non-Go file extensions to their line-comment prefix. +// This is used so that files in languages other than Go can carry +// "go:build" constraints using their own comment syntax, without +// requiring the //go:build form which is invalid in those languages. +// +// For example, a MATLAB .m file can use: +// +// % go:build ignore +// +// and a Python/Shell/R file can use: +// +// # go:build ignore +// +// The map covers only the extensions that Go itself recognises via +// fileListForExt; any extension not listed here falls back to the +// standard //go:build parser (which will simply find nothing and +// therefore not exclude the file). +var extCommentPrefix = map[string][]byte{ + // MATLAB / Objective-C shares .m but only MATLAB needs a different prefix. + // We support % go:build for .m files so that MATLAB source files can + // opt out of Go's build system without needing //go:build (which is + // not valid MATLAB syntax and which would cause MATLAB to reject the file). + ".m": []byte("% go:build"), + // Fortran (free-form) uses ! as its comment character. + ".f90": []byte("! go:build"), + // Assembler files already use // but also accept ; or # on some arches; + // they are handled by the default //go:build path, so not listed here. +} + +// goBuildCommentForExt returns the go:build comment prefix for the given +// file extension, e.g. "% go:build" for .m (MATLAB) files. +// It returns nil for extensions that should use the default //go:build form. +func goBuildCommentForExt(ext string) []byte { + return extCommentPrefix[ext] +} + func isGoBuildComment(line []byte) bool { - if !bytes.HasPrefix(line, goBuildComment) { + return isGoBuildCommentWithPrefix(line, goBuildComment) +} + +// isGoBuildCommentWithPrefix reports whether line is a go:build comment +// introduced by the given comment prefix (e.g. []byte("//go:build") for Go, +// []byte("% go:build") for MATLAB .m files, []byte("# go:build") for +// Python/shell/R files). +// +// The rule mirrors the standard isGoBuildComment: the prefix must be present +// and must be followed by whitespace (or be the entire line), so that e.g. +// "% go:buildmore" is not mistaken for a constraint. +func isGoBuildCommentWithPrefix(line, prefix []byte) bool { + if !bytes.HasPrefix(line, prefix) { return false } line = bytes.TrimSpace(line) - rest := line[len(goBuildComment):] + rest := line[len(prefix):] return len(rest) == 0 || len(bytes.TrimSpace(rest)) < len(rest) } @@ -1568,45 +1624,73 @@ var binaryOnlyComment = []byte("//go:binary-only-package") // shouldBuild reports whether the file should be built // and whether a //go:binary-only-package comment was found. func (ctxt *Context) shouldBuild(content []byte, allTags map[string]bool) (shouldBuild, binaryOnly bool, err error) { - // Identify leading run of // comments and blank lines, - // which must be followed by a blank line. - // Also identify any //go:build comments. - content, goBuild, sawBinaryOnly, err := parseFileHeader(content) + return ctxt.shouldBuildWithPrefix(content, allTags, nil) +} + +// shouldBuildWithPrefix is like shouldBuild but accepts an explicit line-comment +// prefix for non-Go source languages. When commentPrefix is non-nil it is used +// instead of "//go:build" when scanning for build constraints. +// +// For example, MATLAB .m files use "% go:build" as their constraint prefix, +// and Python/shell/R files use "# go:build". Passing the appropriate prefix +// here lets those files opt in or out of a build without requiring "//go:build", +// which is syntactically invalid in those languages. +// +// When commentPrefix is nil the function behaves exactly like shouldBuild and +// uses the standard "//go:build" prefix together with the legacy "// +build" form. +func (ctxt *Context) shouldBuildWithPrefix(content []byte, allTags map[string]bool, commentPrefix []byte) (shouldBuild, binaryOnly bool, err error) { + var goBuild []byte + var sawBinaryOnly bool + if commentPrefix == nil { + // Standard Go path: parseFileHeader also handles legacy // +build. + content, goBuild, sawBinaryOnly, err = parseFileHeader(content) + } else { + // Non-Go path: use the language-specific comment prefix. + content, goBuild, sawBinaryOnly, err = parseFileHeaderWithPrefix(content, commentPrefix) + } if err != nil { return false, false, err } - // If //go:build line is present, it controls. - // Otherwise fall back to +build processing. switch { case goBuild != nil: - x, err := constraint.Parse(string(goBuild)) + // Strip the comment prefix so constraint.Parse sees only the expression. + // Re-wrap it as "//go:build " which constraint.Parse understands. + prefix := goBuildComment + if commentPrefix != nil { + prefix = commentPrefix + } + expr := string(bytes.TrimSpace(goBuild[len(prefix):])) + x, err := constraint.Parse("//go:build " + expr) if err != nil { - return false, false, fmt.Errorf("parsing //go:build line: %v", err) + return false, false, fmt.Errorf("parsing go:build line: %v", err) } shouldBuild = ctxt.eval(x, allTags) default: shouldBuild = true - p := content - for len(p) > 0 { - line := p - if i := bytes.IndexByte(line, '\n'); i >= 0 { - line, p = line[:i], p[i+1:] - } else { - p = p[len(p):] - } - line = bytes.TrimSpace(line) - if !bytes.HasPrefix(line, slashSlash) || !bytes.Contains(line, plusBuild) { - continue - } - text := string(line) - if !constraint.IsPlusBuild(text) { - continue - } - if x, err := constraint.Parse(text); err == nil { - if !ctxt.eval(x, allTags) { - shouldBuild = false + if commentPrefix == nil { + // Legacy // +build processing is only meaningful for Go-style // comments. + p := content + for len(p) > 0 { + line := p + if i := bytes.IndexByte(line, '\n'); i >= 0 { + line, p = line[:i], p[i+1:] + } else { + p = p[len(p):] + } + line = bytes.TrimSpace(line) + if !bytes.HasPrefix(line, slashSlash) || !bytes.Contains(line, plusBuild) { + continue + } + text := string(line) + if !constraint.IsPlusBuild(text) { + continue + } + if x, err := constraint.Parse(text); err == nil { + if !ctxt.eval(x, allTags) { + shouldBuild = false + } } } } @@ -1691,6 +1775,61 @@ Lines: return content[:end], goBuild, sawBinaryOnly, nil } +// parseFileHeaderWithPrefix is like parseFileHeader but is intended for +// non-Go source files that use a different line-comment syntax. +// +// commentPrefix is the language-specific line-comment prefix that introduces a +// go:build constraint in that language, e.g.: +// +// - []byte("% go:build") for MATLAB .m files +// - []byte("# go:build") for Python / shell / R files +// - []byte("! go:build") for free-form Fortran files +// +// The function scans the leading lines of content for a line that matches +// commentPrefix followed by a build expression. Because non-Go languages +// don't have /* */ block comments the slash-star tracking used by +// parseFileHeader is omitted here; each line is inspected independently. +// +// Only one go:build line is accepted; a second one is an error, consistent +// with parseFileHeader. +func parseFileHeaderWithPrefix(content []byte, commentPrefix []byte) (trimmed, goBuild []byte, sawBinaryOnly bool, err error) { + end := 0 + p := content + ended := false // found a non-blank, non-comment line + +Lines: + for len(p) > 0 { + line := p + if i := bytes.IndexByte(line, '\n'); i >= 0 { + line, p = line[:i], p[i+1:] + } else { + p = p[len(p):] + } + line = bytes.TrimSpace(line) + if len(line) == 0 && !ended { + // Blank line: update the candidate end position. + end = len(content) - len(p) + continue Lines + } + if !bytes.HasPrefix(line, commentPrefix[:1]) { + // First byte of the comment prefix doesn't match → not a comment line. + ended = true + continue Lines + } + // It starts with the comment character; check for the full prefix. + if isGoBuildCommentWithPrefix(line, commentPrefix) { + if goBuild != nil { + return nil, nil, false, errMultipleGoBuild + } + goBuild = line + } + // Non-Go files don't have //go:binary-only-package, so sawBinaryOnly + // stays false. + } + + return content[:end], goBuild, sawBinaryOnly, nil +} + // saveCgo saves the information from the #cgo lines in the import "C" comment. // These lines set CFLAGS, CPPFLAGS, CXXFLAGS and LDFLAGS and pkg-config directives // that affect the way cgo's C code is built. diff --git a/src/go/build/build_test.go b/src/go/build/build_test.go index 09fbeebdc701b1..019e7be33f25ec 100644 --- a/src/go/build/build_test.go +++ b/src/go/build/build_test.go @@ -341,6 +341,148 @@ var shouldBuildTests = []struct { }, } +// shouldBuildWithPrefixTests tests build-constraint recognition in non-Go +// source files that use a language-specific comment prefix instead of //. +var shouldBuildWithPrefixTests = []struct { + name string + content string + commentPrefix string // e.g. "% go:build" for MATLAB + tags map[string]bool + shouldBuild bool + err error +}{ + // --- MATLAB .m files (prefix "% go:build") --- + { + name: "MatlabIgnore", + content: "% go:build ignore\n\nfunction r = f()\n r = 1;\nend\n", + commentPrefix: "% go:build", + tags: map[string]bool{"ignore": true}, + shouldBuild: false, + }, + { + name: "MatlabNoConstraint", + content: "% Plain MATLAB file with no build tag.\nfunction r = f()\n r = 1;\nend\n", + commentPrefix: "% go:build", + tags: map[string]bool{}, + shouldBuild: true, + }, + { + name: "MatlabLinux", + content: "% go:build linux\n\nfunction r = f()\n r = 1;\nend\n", + commentPrefix: "% go:build", + tags: map[string]bool{"linux": true}, + shouldBuild: true, + }, + { + name: "MatlabLinuxFalse", + content: "% go:build linux\n\nfunction r = f()\n r = 1;\nend\n", + commentPrefix: "% go:build", + tags: map[string]bool{}, + shouldBuild: false, + }, + { + name: "MatlabAnd", + content: "% go:build linux && amd64\n\nfunction r = f()\n r = 1;\nend\n", + commentPrefix: "% go:build", + tags: map[string]bool{"linux": true, "amd64": true}, + shouldBuild: true, + }, + { + name: "MatlabNot", + content: "% go:build !ignore\n\nfunction r = f()\n r = 1;\nend\n", + commentPrefix: "% go:build", + tags: map[string]bool{"ignore": false}, + shouldBuild: true, + }, + // go:build lookalike that is NOT a valid constraint should NOT end the scan + // prematurely (e.g. "% go:buildmore" is not a constraint). + { + name: "MatlabNotAConstraint", + content: "% go:buildmore something\n\nfunction r = f()\n r = 1;\nend\n", + commentPrefix: "% go:build", + tags: map[string]bool{}, + shouldBuild: true, + }, + // --- Shell / Python / R files (prefix "# go:build") --- + { + name: "HashIgnore", + content: "# go:build ignore\n\necho hello\n", + commentPrefix: "# go:build", + tags: map[string]bool{"ignore": true}, + shouldBuild: false, + }, + { + name: "HashNoConstraint", + content: "# Just a shell comment.\necho hello\n", + commentPrefix: "# go:build", + tags: map[string]bool{}, + shouldBuild: true, + }, +} + +func TestShouldBuildWithPrefix(t *testing.T) { + for _, tt := range shouldBuildWithPrefixTests { + t.Run(tt.name, func(t *testing.T) { + ctx := &Context{BuildTags: []string{"yes"}} + tags := map[string]bool{} + shouldBuild, _, err := ctx.shouldBuildWithPrefix([]byte(tt.content), tags, []byte(tt.commentPrefix)) + if shouldBuild != tt.shouldBuild || !maps.Equal(tags, tt.tags) || err != tt.err { + t.Errorf("mismatch:\n"+ + "have shouldBuild=%v, tags=%v, err=%v\n"+ + "want shouldBuild=%v, tags=%v, err=%v", + shouldBuild, tags, err, + tt.shouldBuild, tt.tags, tt.err) + } + }) + } +} + +// TestMatlabFileBuildTags verifies that .m files in a package directory are +// correctly included or excluded based on "% go:build" constraints, and that +// the constraints do NOT interfere with MATLAB being able to read the file +// (i.e. we do not require the //go:build form). +func TestMatlabFileBuildTags(t *testing.T) { + ctxt := Default + ctxt.GOOS = "linux" + ctxt.GOARCH = "amd64" + + p, err := ctxt.ImportDir("testdata/non_go_build_tags", 0) + if err != nil { + t.Fatalf("ImportDir: %v", err) + } + + // included.m has no constraint → must be in MFiles. + wantIncluded := "included.m" + foundIncluded := false + for _, f := range p.MFiles { + if f == wantIncluded { + foundIncluded = true + } + } + if !foundIncluded { + t.Errorf("expected %q in MFiles, got %v", wantIncluded, p.MFiles) + } + + // ignored.m has "% go:build ignore" → must NOT be in MFiles. + for _, f := range p.MFiles { + if f == "ignored.m" { + t.Errorf("ignored.m should have been excluded from MFiles (has %% go:build ignore)") + } + } + + // linux_only.m has "% go:build linux" and we're on linux → must be included. + wantLinux := "linux_only.m" + foundLinux := false + for _, f := range p.MFiles { + if f == wantLinux { + foundLinux = true + } + } + if !foundLinux { + t.Errorf("expected %q in MFiles on linux, got %v", wantLinux, p.MFiles) + } +} + func TestShouldBuild(t *testing.T) { for _, tt := range shouldBuildTests { t.Run(tt.name, func(t *testing.T) { diff --git a/src/go/build/testdata/non_go_build_tags/doc.go b/src/go/build/testdata/non_go_build_tags/doc.go new file mode 100644 index 00000000000000..a3fc5b4b77c373 --- /dev/null +++ b/src/go/build/testdata/non_go_build_tags/doc.go @@ -0,0 +1,3 @@ +// Package non_go_build_tags tests that build constraints in non-Go +// source files using language-specific comment prefixes are honoured. +package non_go_build_tags diff --git a/src/go/build/testdata/non_go_build_tags/ignored.m b/src/go/build/testdata/non_go_build_tags/ignored.m new file mode 100644 index 00000000000000..d85d324f86e2c1 --- /dev/null +++ b/src/go/build/testdata/non_go_build_tags/ignored.m @@ -0,0 +1,5 @@ +% go:build ignore + +function result = add(a, b) + result = a + b; +end diff --git a/src/go/build/testdata/non_go_build_tags/included.m b/src/go/build/testdata/non_go_build_tags/included.m new file mode 100644 index 00000000000000..e066822e3eee53 --- /dev/null +++ b/src/go/build/testdata/non_go_build_tags/included.m @@ -0,0 +1,4 @@ +% This is a plain MATLAB file with no build constraints. +function result = multiply(a, b) + result = a * b; +end diff --git a/src/go/build/testdata/non_go_build_tags/linux_only.m b/src/go/build/testdata/non_go_build_tags/linux_only.m new file mode 100644 index 00000000000000..7bffc750a1f79b --- /dev/null +++ b/src/go/build/testdata/non_go_build_tags/linux_only.m @@ -0,0 +1,5 @@ +% go:build linux + +function result = linuxHelper() + result = 42; +end