From ab183bbfb54e84dc0c70f36409f1d62de794aec5 Mon Sep 17 00:00:00 2001 From: Solomon Hykes Date: Mon, 1 Jun 2026 21:49:36 +0000 Subject: [PATCH 1/6] Add Go module collections Signed-off-by: Solomon Hykes --- .dagger/modules/e2e/main.dang | 33 +++++++++++-------- go.dang | 62 +++++++++++++++++++++++++++++++---- 2 files changed, 75 insertions(+), 20 deletions(-) diff --git a/.dagger/modules/e2e/main.dang b/.dagger/modules/e2e/main.dang index 6d1c9b9..a6a5757 100644 --- a/.dagger/modules/e2e/main.dang +++ b/.dagger/modules/e2e/main.dang @@ -46,10 +46,8 @@ type E2e { .length > 0 } - let containsModulePath(mods: [{{path: String!}}!]!, want: String!): Boolean! { - mods - .filter { mod => mod.path == want } - .length > 0 + let containsModulePath(paths: [String!]!, want: String!): Boolean! { + containsPath(paths, want) } """ @@ -112,7 +110,7 @@ type E2e { let testDirs = mod.testDirectories(ws) assert( - containsModulePath(testDirs.{path}, baseFixturePath), + containsModulePath(testDirs.{path}.map { dir => dir.path }, baseFixturePath), "custom base test directory discovery did not include expected path", ) @@ -157,7 +155,8 @@ type E2e { ) let tool = go(version: "1.26.1", skipLint: skipLint, skipTest: skipTest, skipGenerate: skipGenerate) - let allModulePaths = tool.modules(ws).{path} + let allModules = tool.modules(ws) + let allModulePaths = allModules.keys assert( containsModulePath(allModulePaths, "fixtures/go-module-cross-include-a"), "unfiltered modules did not include expected fixture module", @@ -170,10 +169,18 @@ type E2e { containsModulePath(allModulePaths, "testdata/go-module-excluded"), "unfiltered modules did not include skipped fixture module by default", ) + assert( + allModules.get(key: "testdata/go-module-with-testdata").path == "testdata/go-module-with-testdata", + "module collection get did not return the requested module", + ) + assert( + allModules.subset(keys: ["fixtures/go-module-cross-include-a"]).keys.length == 1, + "module collection subset did not narrow to the requested keys", + ) let includedModulePaths = tool .modules(ws, include: ["fixtures/go-module-cross-include-a"]) - .{path} + .keys assert( containsModulePath(includedModulePaths, "fixtures/go-module-cross-include-a"), "modules include filter did not include matching module root", @@ -185,7 +192,7 @@ type E2e { let excludedModulePaths = tool .modules(ws, exclude: ["fixtures/go-module-cross-include-a"]) - .{path} + .keys assert( containsModulePath(excludedModulePaths, "fixtures/go-module-cross-include-a") == false, "modules exclude filter included excluded module root", @@ -197,7 +204,7 @@ type E2e { include: ["fixtures/go-module-cross-include-*"], exclude: ["fixtures/go-module-cross-include-b"], ) - .{path} + .keys assert( containsModulePath(combinedModulePaths, "fixtures/go-module-cross-include-a"), "modules combined filters did not include matching module root", @@ -209,7 +216,7 @@ type E2e { let contentMatchedModulePaths = tool .modules(ws, include: ["**/module-a-only.data"]) - .{path} + .keys assert( containsModulePath(contentMatchedModulePaths, "fixtures/go-module-cross-include-a"), "modules include filter did not match module directory contents", @@ -221,7 +228,7 @@ type E2e { let lintSkippedModulePaths = tool .modules(ws, includeSkipLint: false) - .{path} + .keys assert( containsModulePath(lintSkippedModulePaths, "testdata/go-module-excluded") == false, "modules includeSkipLint false included lint-skipped module", @@ -241,7 +248,7 @@ type E2e { let testSkippedModulePaths = tool .modules(ws, includeSkipTest: false) - .{path} + .keys assert( containsModulePath(testSkippedModulePaths, "testdata/go-module-excluded") == false, "modules includeSkipTest false included test-skipped module", @@ -261,7 +268,7 @@ type E2e { let generateSkippedModulePaths = tool .modules(ws, includeSkipGenerate: false) - .{path} + .keys assert( containsModulePath(generateSkippedModulePaths, "testdata/go-module-excluded") == false, "modules includeSkipGenerate false included generate-skipped module", diff --git a/go.dang b/go.dang index 815af65..6416839 100644 --- a/go.dang +++ b/go.dang @@ -80,7 +80,8 @@ type Go { pub skipGenerate: [String!]! = [] """ - Return every Go module discovered from workspace go.mod files. + Return Go modules discovered from workspace go.mod files, as a collection + keyed by module root path. Optional include/exclude patterns filter discovered module root directories. """ pub modules( @@ -90,8 +91,8 @@ type Go { includeSkipLint: Boolean! = true, includeSkipTest: Boolean! = true, includeSkipGenerate: Boolean! = true, - ): [GoModule!]! { - ws + ): GoModules! { + let paths = ws .directory("/", include: ["**/go.mod"]) .glob("**/go.mod") .map { modPath => @@ -116,6 +117,17 @@ type Go { (includeSkipTest or mod.skipTest(ws) == false) and (includeSkipGenerate or mod.skipGenerate(ws) == false) } + .map { mod => mod.path } + + GoModules( + paths: paths, + version: version, + baseImage: base, + includeExtraFiles: includeExtraFiles, + skipLintPaths: skipLint, + skipTestPaths: skipTest, + skipGeneratePaths: skipGenerate, + ) } let modulePathIncluded( @@ -171,7 +183,9 @@ type Go { Modules configured to skip lint are skipped. """ pub lintAll(ws: Workspace!): Void @check { - let layers = modules(ws, includeSkipLint: false).reduce(directory) { layers, mod => + let mods = modules(ws, includeSkipLint: false) + let layers = mods.paths.reduce(directory) { layers, path => + let mod = mods.module(path) layers.withDirectory( mod.path, directory.withFile("go.mod", mod.lintExec(ws).file("go.mod")), @@ -190,8 +204,9 @@ type Go { Modules configured to skip tests are skipped. """ pub testAll(ws: Workspace!): Void @check { - let mods = modules(ws, includeSkipTest: false) - let layers = mods.reduce(directory) { layers, mod => + let collection = modules(ws, includeSkipTest: false) + let layers = collection.paths.reduce(directory) { layers, path => + let mod = collection.module(path) layers.withDirectory( mod.path, directory.withFile("go.mod", mod.testExec(ws).file("go.mod")), @@ -211,7 +226,8 @@ type Go { are skipped. """ pub generateAll(ws: Workspace!): Changeset! @generate { - let mods = modules(ws).filter { mod => + let collection = modules(ws) + let mods = collection.paths.map { path => collection.module(path) }.filter { mod => mod.skipGenerate(ws) == false and mod.hasGenerateDirectives(ws) } @@ -231,6 +247,38 @@ type Go { } +""" +Go modules discovered in a workspace, keyed by module root path. +""" +type GoModules { + """ + Workspace-relative module root paths in discovery order. + """ + pub paths: [String!]! @keys + + let version: String + let baseImage: Container! + let includeExtraFiles: [String!]! + let skipLintPaths: [String!]! + let skipTestPaths: [String!]! + let skipGeneratePaths: [String!]! + + """ + Return the discovered Go module with the given workspace-relative root path. + """ + pub module(path: String!): GoModule! @get { + GoModule( + path: path, + version: version, + baseImage: baseImage, + includeExtraFiles: includeExtraFiles, + skipLintPaths: skipLintPaths, + skipTestPaths: skipTestPaths, + skipGeneratePaths: skipGeneratePaths, + ) + } +} + """ A Go module rooted at a workspace-relative path. """ From eaf9fc01efbf5b0043da4c2f1da2363eea468b81 Mon Sep 17 00:00:00 2001 From: Solomon Hykes Date: Wed, 10 Jun 2026 10:30:46 -0700 Subject: [PATCH 2/6] Add test directory and test collections Layer two more collections under GoModules, giving Go workspaces three selection dimensions: go-module, go-directory, and go-test. GoModule.testDirs exposes the existing test directory discovery as a collection keyed by workspace-relative path. Each GoDirectory exposes its tests as a collection keyed by test name, enumerated with go test -list; skipped directories produce an empty collection. Test execution is batched: the collection-level run executes one go test -run '^(A|B)$' over the current subset, and shadows the per-test run so that selecting several tests by name (dagger check --go-test=A --go-test=B) compiles and runs them in a single invocation. Filters push down, so only selected directories enumerate their tests. Note: test directory discovery (pre-existing go-includes behavior) does not surface test files at a module root; only subdirectories containing _test.go files become test directories. Signed-off-by: Solomon Hykes --- go.dang | 152 ++++++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 142 insertions(+), 10 deletions(-) diff --git a/go.dang b/go.dang index 6416839..f55d247 100644 --- a/go.dang +++ b/go.dang @@ -435,6 +435,19 @@ type GoModule { Directories in this module containing Go test files. """ pub testDirectories(ws: Workspace!): [GoDirectory!]! { + testDirectoryPaths(ws).map { testPath => + GoDirectory( + path: testPath, + ws: ws, + modulePath: path, + baseImage: baseImage, + includeExtraFiles: includeExtraFiles, + skipTestPaths: skipTestPaths, + ) + } + } + + let testDirectoryPaths(ws: Workspace!): [String!]! { goIncludesHelper(ws) .withExec( ["go-includes", "--output", "/output", "--test-dirs", workspacePath], @@ -444,16 +457,20 @@ type GoModule { .contents .split("\n") .filter { testPath => testPath != "" } - .map { testPath => - GoDirectory( - path: testPath, - ws: ws, - modulePath: path, - baseImage: baseImage, - includeExtraFiles: includeExtraFiles, - skipTestPaths: skipTestPaths, - ) - } + } + + """ + The test directories of this module, as a collection keyed by path. + """ + pub testDirs(ws: Workspace!): GoTestDirs! { + GoTestDirs( + paths: testDirectoryPaths(ws), + ws: ws, + modulePath: path, + baseImage: baseImage, + includeExtraFiles: includeExtraFiles, + skipTestPaths: skipTestPaths, + ) } """ @@ -649,6 +666,36 @@ type GoModule { """ A workspace directory containing Go test files. """ +""" +Test directories of a Go module, keyed by workspace-relative path. +""" +type GoTestDirs { + """ + Workspace-relative test directory paths in discovery order. + """ + pub paths: [String!]! @keys + + let ws: Workspace! + let modulePath: String! + let baseImage: Container! + let includeExtraFiles: [String!]! + let skipTestPaths: [String!]! + + """ + Return the test directory with the given workspace-relative path. + """ + pub directory(path: String!): GoDirectory! @get { + GoDirectory( + path: path, + ws: ws, + modulePath: modulePath, + baseImage: baseImage, + includeExtraFiles: includeExtraFiles, + skipTestPaths: skipTestPaths, + ) + } +} + type GoDirectory { """ Workspace-relative path of this directory. @@ -805,4 +852,89 @@ type GoDirectory { null } + """ + Go test container for this directory, before a test command is added. + """ + pub testContainer(): Container! { + base + .withDirectory(".", source) + .withDirectory(".", testData) + .withWorkdir(path) + } + + """ + The tests in this directory, as a collection keyed by test name. + Skipped directories produce an empty collection. + """ + pub tests(): GoTests! { + let names = if (skipTest) { + [] + } else { + testContainer() + .withExec(["sh", "-c", "go test -list '.*' . | grep '^Test' || true"]) + .stdout + .split("\n") + .filter { name => name != "" } + } + GoTests(names: names, dir: self) + } + +} + +""" +Tests in a Go directory, keyed by test name. +""" +type GoTests { + """ + Test names in listing order. + """ + pub names: [String!]! @keys + + let dir: GoDirectory! + + """ + Return the test with the given name. + """ + pub test(name: String!): GoTest! @get { + GoTest(name: name, dir: dir) + } + + """ + Run every test in the current subset in a single go test invocation. + """ + pub run(): Void @check { + if (names.length == 0) { + null + } else { + dir + .testContainer() + .withExec(["go", "test", "-run", "^(" + names.join("|") + ")$", "."]) + .sync + null + } + } +} + +""" +A single Go test. +""" +type GoTest { + """ + The test's name. + """ + pub name: String! + + let dir: GoDirectory! + + """ + Run this test. Batched through the collection when several tests are + selected together. + """ + pub run(): Void @check { + dir + .testContainer() + .withExec(["go", "test", "-run", "^" + name + "$", "."]) + .sync + null + } } From 6ea03faa593c2fb80c9333d4934547831e70ef79 Mon Sep 17 00:00:00 2001 From: Solomon Hykes Date: Thu, 11 Jun 2026 16:25:03 -0700 Subject: [PATCH 3/6] Discover Go tests by static regex instead of go test -list GoDirectory.tests used to spin up a Go container and parse 'go test -list' stdout to enumerate test names. Replace that with a single Workspace.directory(...).search(pattern: "^func Test\\w+\\(") over the directory's *_test.go files and parse names from the matched lines in Dang. This is much faster (no compile, no container exec) and matches the same set go test -list does for top-level test functions. Test methods on testify-style suites are intentionally not listed: they cannot be selected with go test -run independently of their parent function. Add moduleIntrospectionCheck assertions covering the new GoTestDirs and GoTests collection surface end-to-end: keys/get round-trip on both levels, multi-test discovery from a single *_test.go file (cross- include-b has two top-level tests), and that a skip-configured directory produces an empty tests collection. Signed-off-by: Solomon Hykes --- .dagger/modules/e2e/main.dang | 50 +++++++++++++++++++++++++++++++++++ go.dang | 19 ++++++++++--- 2 files changed, 65 insertions(+), 4 deletions(-) diff --git a/.dagger/modules/e2e/main.dang b/.dagger/modules/e2e/main.dang index a6a5757..1ea2c50 100644 --- a/.dagger/modules/e2e/main.dang +++ b/.dagger/modules/e2e/main.dang @@ -433,6 +433,56 @@ type E2e { ), "go:generate scoped include did not include generator support files", ) + + # Test directory collection surface and static test discovery. + # cross-include-b has two top-level Test functions in a single file — + # exercises ripgrep-based listing and multi-match handling. + let crossB = tool.module(ws, "fixtures/go-module-cross-include-b") + let crossBTestDirs = crossB.testDirs(ws) + assert( + containsPath(crossBTestDirs.keys, "fixtures/go-module-cross-include-b"), + "testDirs collection did not include expected test directory", + ) + assert( + crossBTestDirs + .get(key: "fixtures/go-module-cross-include-b") + .path == "fixtures/go-module-cross-include-b", + "testDirs collection get did not return the requested directory", + ) + + let crossBTests = crossBTestDirs + .get(key: "fixtures/go-module-cross-include-b") + .tests() + assert( + crossBTests.keys.length == 2, + "tests collection did not discover expected number of tests", + ) + assert( + containsPath(crossBTests.keys, "TestSiblingModuleDirectiveIncludeIsNotMounted"), + "tests collection did not include expected test name", + ) + assert( + containsPath(crossBTests.keys, "TestSiblingModuleTestdataIsNotMounted"), + "tests collection did not include second test from same file", + ) + assert( + crossBTests + .get(key: "TestSiblingModuleDirectiveIncludeIsNotMounted") + .name == "TestSiblingModuleDirectiveIncludeIsNotMounted", + "tests collection get did not return the requested test", + ) + + # Skip-configured directory should report no tests at all. + let skippedTests = tool + .module(ws, "testdata/go-module-excluded") + .testDirs(ws) + .get(key: "testdata/go-module-excluded") + .tests() + assert( + skippedTests.keys.length == 0, + "tests collection on a skipped directory was not empty", + ) + null } diff --git a/go.dang b/go.dang index f55d247..9aa5019 100644 --- a/go.dang +++ b/go.dang @@ -865,15 +865,26 @@ type GoDirectory { """ The tests in this directory, as a collection keyed by test name. Skipped directories produce an empty collection. + + Test names are discovered by statically matching top-level test function + declarations in *_test.go files, without compiling or running go test. """ pub tests(): GoTests! { let names = if (skipTest) { [] } else { - testContainer() - .withExec(["sh", "-c", "go test -list '.*' . | grep '^Test' || true"]) - .stdout - .split("\n") + let glob = if (path == ".") { + "*_test.go" + } else { + path.trimSuffix("/") + "/*_test.go" + } + ws + .directory("/", include: [glob]) + .search(pattern: "^func Test\\w+\\(") + .{matchedLines} + .map { result => + result.matchedLines.trimSpace.trimPrefix("func ").split("(")[0] ?? "" + } .filter { name => name != "" } } GoTests(names: names, dir: self) From 6ce779b40babcdb2de5c7a014978636006c58df2 Mon Sep 17 00:00:00 2001 From: Solomon Hykes Date: Thu, 11 Jun 2026 22:18:52 -0700 Subject: [PATCH 4/6] Materialize test files at the Workspace boundary, share downstream Every function with a Workspace receiver or argument is invalidated unconditionally. GoDirectory.tests() was crossing Workspace once per test directory, which on dagger/dagger meant ~hundreds of invalidated calls and ~12 minutes to enumerate go-test. Thread a single Directory snapshot of all **/*_test.go files from the Workspace boundary (Go.modules / Go.module) down through GoModules -> GoModule -> GoTestDirs -> GoDirectory. GoDirectory.tests() now searches that pre-materialized Directory and filters results by filePath. The search arguments are identical for every directory, so the search hits the dagql cache (Directory receiver -> content-addressed) after the first call; per-directory restriction is pure-Dang on the cached result. Net: one Workspace crossing and one ripgrep per session, no matter how many directories the workspace contains. Signed-off-by: Solomon Hykes --- go.dang | 53 +++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 45 insertions(+), 8 deletions(-) diff --git a/go.dang b/go.dang index 9aa5019..ae991f8 100644 --- a/go.dang +++ b/go.dang @@ -92,6 +92,12 @@ type Go { includeSkipTest: Boolean! = true, includeSkipGenerate: Boolean! = true, ): GoModules! { + # Single workspace-wide snapshot of every *_test.go file, materialized + # once at this Workspace boundary and threaded down through the + # collection so GoDirectory.tests() can search/filter without ever + # touching Workspace again. Functions taking Workspace always invalidate; + # operations on a Directory receiver cache by content hash. + let testFilesDir = ws.directory("/", include: ["**/*_test.go"]) let paths = ws .directory("/", include: ["**/go.mod"]) .glob("**/go.mod") @@ -110,6 +116,7 @@ type Go { skipLintPaths: skipLint, skipTestPaths: skipTest, skipGeneratePaths: skipGenerate, + testFilesDir: testFilesDir, ) } .filter { mod => @@ -127,6 +134,7 @@ type Go { skipLintPaths: skipLint, skipTestPaths: skipTest, skipGeneratePaths: skipGenerate, + testFilesDir: testFilesDir, ) } @@ -175,6 +183,7 @@ type Go { skipLintPaths: skipLint, skipTestPaths: skipTest, skipGeneratePaths: skipGenerate, + testFilesDir: ws.directory("/", include: ["**/*_test.go"]), ) } @@ -262,6 +271,7 @@ type GoModules { let skipLintPaths: [String!]! let skipTestPaths: [String!]! let skipGeneratePaths: [String!]! + let testFilesDir: Directory! """ Return the discovered Go module with the given workspace-relative root path. @@ -275,6 +285,7 @@ type GoModules { skipLintPaths: skipLintPaths, skipTestPaths: skipTestPaths, skipGeneratePaths: skipGeneratePaths, + testFilesDir: testFilesDir, ) } } @@ -318,6 +329,13 @@ type GoModule { """ let skipGeneratePaths: [String!]! + """ + Workspace-wide snapshot of every *_test.go file, threaded down from the + Go.modules / Go.module constructor. Used by GoDirectory.tests so that + test discovery never crosses Workspace again. + """ + let testFilesDir: Directory! + """ Return a workspace-root path for a module-relative subpath. """ @@ -443,6 +461,7 @@ type GoModule { baseImage: baseImage, includeExtraFiles: includeExtraFiles, skipTestPaths: skipTestPaths, + testFilesDir: testFilesDir, ) } } @@ -470,6 +489,7 @@ type GoModule { baseImage: baseImage, includeExtraFiles: includeExtraFiles, skipTestPaths: skipTestPaths, + testFilesDir: testFilesDir, ) } @@ -680,6 +700,7 @@ type GoTestDirs { let baseImage: Container! let includeExtraFiles: [String!]! let skipTestPaths: [String!]! + let testFilesDir: Directory! """ Return the test directory with the given workspace-relative path. @@ -692,6 +713,7 @@ type GoTestDirs { baseImage: baseImage, includeExtraFiles: includeExtraFiles, skipTestPaths: skipTestPaths, + testFilesDir: testFilesDir, ) } } @@ -727,6 +749,13 @@ type GoDirectory { """ let skipTestPaths: [String!]! + """ + Workspace-wide snapshot of every *_test.go file, threaded down from the + parent collection. tests() searches/filters this directory so the cost + is paid once per session (Directory receiver = content-addressed cache). + """ + let testFilesDir: Directory! + """ Return a workspace-root path for a module-relative subpath. """ @@ -868,20 +897,28 @@ type GoDirectory { Test names are discovered by statically matching top-level test function declarations in *_test.go files, without compiling or running go test. + + Search runs against testFilesDir, a Directory threaded from the parent + collection. The search arguments are identical for every GoDirectory, + so the result is cached once per session and reused; per-directory + restriction is a pure-Dang filter on the returned filePath. """ pub tests(): GoTests! { let names = if (skipTest) { [] } else { - let glob = if (path == ".") { - "*_test.go" - } else { - path.trimSuffix("/") + "/*_test.go" - } - ws - .directory("/", include: [glob]) + let prefix = if (path == ".") { "" } else { path.trimSuffix("/") + "/" } + testFilesDir .search(pattern: "^func Test\\w+\\(") - .{matchedLines} + .{filePath, matchedLines} + .filter { result => + if (path == ".") { + result.filePath.contains("/") == false + } else { + result.filePath.hasPrefix(prefix) and + result.filePath.trimPrefix(prefix).contains("/") == false + } + } .map { result => result.matchedLines.trimSpace.trimPrefix("func ").split("(")[0] ?? "" } From 2402ad114b0f7f1e5f10d8a3f5c9ee2fe2c15d44 Mon Sep 17 00:00:00 2001 From: Matias Pan Date: Thu, 2 Jul 2026 14:31:59 -0300 Subject: [PATCH 5/6] dang: route go collections through v2 and normalize skips --- .dagger/modules/e2e/dagger-module.toml | 2 +- .dagger/modules/e2e/main.dang | 49 ++++++++++++++++++- dagger.json | 2 +- go.dang | 31 ++++++++++-- testdata/go-module-cgo-cxx/answer.cc | 3 ++ testdata/go-module-cgo-cxx/cgocxx.go | 8 +++ testdata/go-module-cgo-cxx/go.mod | 3 ++ testdata/go-module-lint-fail/go.mod | 3 ++ testdata/go-module-lint-fail/lint_fail.go | 5 ++ .../go-module-with-testdata/nesting_test.go | 8 +-- 10 files changed, 102 insertions(+), 12 deletions(-) create mode 100644 testdata/go-module-cgo-cxx/answer.cc create mode 100644 testdata/go-module-cgo-cxx/cgocxx.go create mode 100644 testdata/go-module-cgo-cxx/go.mod create mode 100644 testdata/go-module-lint-fail/go.mod create mode 100644 testdata/go-module-lint-fail/lint_fail.go diff --git a/.dagger/modules/e2e/dagger-module.toml b/.dagger/modules/e2e/dagger-module.toml index 79cf559..d80999d 100644 --- a/.dagger/modules/e2e/dagger-module.toml +++ b/.dagger/modules/e2e/dagger-module.toml @@ -1,5 +1,5 @@ name = "e2e" -engineVersion = "v0.20.6" +engineVersion = "v0.21.5" [runtime] source = "dang" diff --git a/.dagger/modules/e2e/main.dang b/.dagger/modules/e2e/main.dang index 1ea2c50..46edc7a 100644 --- a/.dagger/modules/e2e/main.dang +++ b/.dagger/modules/e2e/main.dang @@ -7,15 +7,21 @@ type E2e { let baseFixturePath: String! = "testdata/go-module-custom-base" let baseGeneratedFilePath: String! = baseFixturePath + "/generated.go" let emptyFixturePath: String! = "testdata/go-module-empty" + let cgoCxxFixturePath: String! = "testdata/go-module-cgo-cxx" + let lintFailureFixturePath: String! = "testdata/go-module-lint-fail" let skipLint: [String!]! = [ baseFixturePath, + cgoCxxFixturePath, emptyFixturePath, + lintFailureFixturePath, "testdata/go-module-excluded", "testdata/go-module-skip-tree", ] let skipTest: [String!]! = [ baseFixturePath, + cgoCxxFixturePath, emptyFixturePath, + lintFailureFixturePath, "testdata/go-module-excluded", "testdata/go-module-skip-tree", ] @@ -84,6 +90,13 @@ type E2e { go(version: "1.26.1").module(ws, "fixtures/go-module-with-replace").test(ws) } + """ + Lint can typecheck cgo packages that need a C++ compiler. + """ + pub cgoCxxLintCheck(ws: Workspace!): Void @check { + go(version: "1.26.1").module(ws, cgoCxxFixturePath).lint(ws) + } + """ A custom base container must be used for Go helpers, tests, and generate. """ @@ -110,7 +123,7 @@ type E2e { let testDirs = mod.testDirectories(ws) assert( - containsModulePath(testDirs.{path}.map { dir => dir.path }, baseFixturePath), + containsModulePath(testDirs.{{path}}.map { dir => dir.path }, baseFixturePath), "custom base test directory discovery did not include expected path", ) @@ -282,6 +295,40 @@ type E2e { "modules includeSkipGenerate false excluded non-generate-skipped module", ) + let normalizedSkipTool = go( + version: "1.26.1", + skipLint: ["[\"" + emptyFixturePath + "\"", "\"" + lintFailureFixturePath + "\"]"], + skipTest: ["[\"" + emptyFixturePath + "\"", "\"" + lintFailureFixturePath + "\"]"], + skipGenerate: ["[\"" + emptyFixturePath + "\"", "\"" + lintFailureFixturePath + "\"]"], + ) + assert( + normalizedSkipTool.module(ws, emptyFixturePath).skipLint(ws), + "module did not normalize beta workspace-settings lint skip array fragment", + ) + assert( + normalizedSkipTool.module(ws, lintFailureFixturePath).skipLint(ws), + "module did not normalize trailing beta workspace-settings lint skip array fragment", + ) + assert( + normalizedSkipTool.module(ws, "testdata/go-module-with-testdata").skipLint(ws) == false, + "normalized beta workspace-settings lint skip array skipped unrelated module", + ) + assert( + normalizedSkipTool.module(ws, emptyFixturePath).skipTest(ws), + "module did not normalize beta workspace-settings test skip array fragment", + ) + assert( + normalizedSkipTool.module(ws, lintFailureFixturePath).skipGenerate(ws), + "module did not normalize beta workspace-settings generate skip array fragment", + ) + let normalizedLintSkippedModulePaths = normalizedSkipTool + .modules(ws, includeSkipLint: false) + .keys + assert( + containsModulePath(normalizedLintSkippedModulePaths, lintFailureFixturePath) == false, + "normalized beta workspace-settings lint skip array included skipped module", + ) + let withExtraFiles = go( version: "1.26.1", includeExtraFiles: ["LICENSE"], diff --git a/dagger.json b/dagger.json index e84b172..9f8ca8c 100644 --- a/dagger.json +++ b/dagger.json @@ -1,6 +1,6 @@ { "name": "go", - "engineVersion": "v0.20.6", + "engineVersion": "v0.21.5", "sdk": { "source": "dang" } diff --git a/go.dang b/go.dang index ae991f8..8af3d14 100644 --- a/go.dang +++ b/go.dang @@ -50,13 +50,26 @@ type Go { self.version = if (base == null) { version ?? "1.26" } else { null } self.base = base ?? container.from("golang:" + (version ?? "1.26") + "-alpine") self.includeExtraFiles = includeExtraFiles - self.skipLint = skipLint - self.skipTest = skipTest - self.skipGenerate = skipGenerate + self.skipLint = normalizePatterns(skipLint) + self.skipTest = normalizePatterns(skipTest) + self.skipGenerate = normalizePatterns(skipGenerate) self } } + """ + Normalize selector strings from workspace module settings. + + Dagger v1.0.0-beta.4 may pass TOML string arrays as JSON-fragment strings + like ["[\"infra\"", "\"docs\"]"]; trim those wrappers so selection still + sees the intended patterns. + """ + let normalizePatterns(patterns: [String!]!): [String!]! { + patterns.map { p => + p.trimPrefix("[").trimSuffix("]").trimPrefix("\"").trimSuffix("\"") + } + } + """ Extra workspace-root include patterns mounted for each module's Go commands. Use this for fixtures, generator inputs, or other files not covered by the @@ -405,7 +418,7 @@ type GoModule { literal: true, filesOnly: true, limit: 1, - ).{id} + ).{{id}} .length > 0 } } @@ -510,7 +523,13 @@ type GoModule { [ subpath("**/*.go"), subpath("**/*.c"), + subpath("**/*.cc"), + subpath("**/*.cpp"), + subpath("**/*.cxx"), subpath("**/*.h"), + subpath("**/*.hh"), + subpath("**/*.hpp"), + subpath("**/*.hxx"), subpath("**/*.s"), subpath("**/*.S"), subpath("**/*.syso"), @@ -642,6 +661,8 @@ type GoModule { let lintImage = "docker.io/golangci/golangci-lint:v2.11.4-alpine@sha256:" + "72bcd68512b4e27540dd3a778a1b7afd45759d8145cfb3c089f1d7af53e718e9" container .from(lintImage) + # cgo dependencies may need a C/C++ toolchain during typecheck. + .withExec(["apk", "add", "--no-cache", "build-base"]) .withMountedCache("/go/pkg/mod", cacheVolume("go-mod")) .withMountedCache("/root/.cache/go-build", cacheVolume("go-build")) .withMountedCache("/root/.cache/golangci-lint", cacheVolume("golangci-lint")) @@ -910,7 +931,7 @@ type GoDirectory { let prefix = if (path == ".") { "" } else { path.trimSuffix("/") + "/" } testFilesDir .search(pattern: "^func Test\\w+\\(") - .{filePath, matchedLines} + .{{filePath, matchedLines}} .filter { result => if (path == ".") { result.filePath.contains("/") == false diff --git a/testdata/go-module-cgo-cxx/answer.cc b/testdata/go-module-cgo-cxx/answer.cc new file mode 100644 index 0000000..d58e49e --- /dev/null +++ b/testdata/go-module-cgo-cxx/answer.cc @@ -0,0 +1,3 @@ +extern "C" int answer() { + return 42; +} diff --git a/testdata/go-module-cgo-cxx/cgocxx.go b/testdata/go-module-cgo-cxx/cgocxx.go new file mode 100644 index 0000000..bf4f0bc --- /dev/null +++ b/testdata/go-module-cgo-cxx/cgocxx.go @@ -0,0 +1,8 @@ +package cgocxx + +// int answer(); +import "C" + +func Answer() int { + return int(C.answer()) +} diff --git a/testdata/go-module-cgo-cxx/go.mod b/testdata/go-module-cgo-cxx/go.mod new file mode 100644 index 0000000..b59f791 --- /dev/null +++ b/testdata/go-module-cgo-cxx/go.mod @@ -0,0 +1,3 @@ +module example.com/cgocxx + +go 1.25 diff --git a/testdata/go-module-lint-fail/go.mod b/testdata/go-module-lint-fail/go.mod new file mode 100644 index 0000000..9fbdea5 --- /dev/null +++ b/testdata/go-module-lint-fail/go.mod @@ -0,0 +1,3 @@ +module example.com/lintfail + +go 1.25 diff --git a/testdata/go-module-lint-fail/lint_fail.go b/testdata/go-module-lint-fail/lint_fail.go new file mode 100644 index 0000000..93dda80 --- /dev/null +++ b/testdata/go-module-lint-fail/lint_fail.go @@ -0,0 +1,5 @@ +package lintfail + +func LintFailure() { + unused := 1 +} diff --git a/testdata/go-module-with-testdata/nesting_test.go b/testdata/go-module-with-testdata/nesting_test.go index 0ed3295..4cbe4c4 100644 --- a/testdata/go-module-with-testdata/nesting_test.go +++ b/testdata/go-module-with-testdata/nesting_test.go @@ -28,7 +28,7 @@ func TestDaggerSessionAvailableFromGoTest(t *testing.T) { directory(path: %q) { entries } } currentWorkspace { - cwd + id directory(path: "/", include: ["LICENSE"]) { entries } } }`, wd), @@ -70,7 +70,7 @@ func TestDaggerSessionAvailableFromGoTest(t *testing.T) { } `json:"directory"` } `json:"host"` CurrentWorkspace struct { - Cwd string `json:"cwd"` + ID string `json:"id"` Directory struct { Entries []string `json:"entries"` } `json:"directory"` @@ -90,8 +90,8 @@ func TestDaggerSessionAvailableFromGoTest(t *testing.T) { t.Fatalf("unexpected nested Dagger host directory entries: %v", entries) } - if result.Data.CurrentWorkspace.Cwd != "/testdata/go-module-with-testdata" { - t.Fatalf("unexpected nested Dagger workspace cwd: %q", result.Data.CurrentWorkspace.Cwd) + if result.Data.CurrentWorkspace.ID == "" { + t.Fatal("nested Dagger workspace did not return an ID") } if !contains(result.Data.CurrentWorkspace.Directory.Entries, "LICENSE") { t.Fatalf("unexpected nested Dagger workspace entries: %v", result.Data.CurrentWorkspace.Directory.Entries) From b6403dc21584592cb6a58ff5525aec64cde9bd74 Mon Sep 17 00:00:00 2001 From: Matias Pan Date: Thu, 2 Jul 2026 15:14:51 -0300 Subject: [PATCH 6/6] Add back lint, test and generate selection --- .dagger/modules/e2e/main.dang | 184 +++++++++---- go.dang | 449 +++++++++++++++++++++++++------ helpers/go-includes/all.go | 301 +++++++++++++++++++++ helpers/go-includes/main.go | 32 ++- helpers/go-includes/main_test.go | 6 + 5 files changed, 836 insertions(+), 136 deletions(-) create mode 100644 helpers/go-includes/all.go diff --git a/.dagger/modules/e2e/main.dang b/.dagger/modules/e2e/main.dang index 46edc7a..a042010 100644 --- a/.dagger/modules/e2e/main.dang +++ b/.dagger/modules/e2e/main.dang @@ -9,27 +9,30 @@ type E2e { let emptyFixturePath: String! = "testdata/go-module-empty" let cgoCxxFixturePath: String! = "testdata/go-module-cgo-cxx" let lintFailureFixturePath: String! = "testdata/go-module-lint-fail" - let skipLint: [String!]! = [ - baseFixturePath, - cgoCxxFixturePath, - emptyFixturePath, - lintFailureFixturePath, - "testdata/go-module-excluded", - "testdata/go-module-skip-tree", + let lint: [String!]! = [ + "**", + "!" + baseFixturePath, + "!" + cgoCxxFixturePath, + "!" + emptyFixturePath, + "!" + lintFailureFixturePath, + "!testdata/go-module-excluded", + "!testdata/go-module-skip-tree", ] - let skipTest: [String!]! = [ - baseFixturePath, - cgoCxxFixturePath, - emptyFixturePath, - lintFailureFixturePath, - "testdata/go-module-excluded", - "testdata/go-module-skip-tree", + let test: [String!]! = [ + "**", + "!" + baseFixturePath, + "!" + cgoCxxFixturePath, + "!" + emptyFixturePath, + "!" + lintFailureFixturePath, + "!testdata/go-module-excluded", + "!testdata/go-module-skip-tree", ] - let skipGenerate: [String!]! = [ - "helpers/go-includes", - baseFixturePath, - "testdata/go-module-excluded", - "testdata/go-module-skip-tree", + let generate: [String!]! = [ + "**", + "!helpers/go-includes", + "!" + baseFixturePath, + "!testdata/go-module-excluded", + "!testdata/go-module-skip-tree", ] let base: Container! { @@ -56,11 +59,27 @@ type E2e { containsPath(paths, want) } + """ + Whether a module at modPath is excluded from generate selection `patterns`. + Uses findUp: false so modPath need not be a real module on disk. + """ + let selectsOut(ws: Workspace!, patterns: [String!]!, modPath: String!): Boolean! { + go(version: "1.26.1", generate: patterns).module(ws, modPath, findUp: false).skipGenerate(ws) + } + + """ + Whether a module at modPath is excluded from lint selection `patterns`. + Uses findUp: false so modPath need not be a real module on disk. + """ + let lintSelectsOut(ws: Workspace!, patterns: [String!]!, modPath: String!): Boolean! { + go(version: "1.26.1", lint: patterns).module(ws, modPath, findUp: false).skipLint(ws) + } + """ Run testAll while respecting per-module skip configuration. """ pub singleModuleCheck(ws: Workspace!): Void @check { - let tool = go(version: "1.26.1", skipLint: skipLint, skipTest: skipTest, skipGenerate: skipGenerate) + let tool = go(version: "1.26.1", lint: lint, test: test, generate: generate) tool.lintAll(ws) tool.testAll(ws) tool.module(ws, "testdata/go-module-excluded").lint(ws) @@ -167,7 +186,7 @@ type E2e { "module lookup unexpectedly snapped with findUp false: " + direct.path, ) - let tool = go(version: "1.26.1", skipLint: skipLint, skipTest: skipTest, skipGenerate: skipGenerate) + let tool = go(version: "1.26.1", lint: lint, test: test, generate: generate) let allModules = tool.modules(ws) let allModulePaths = allModules.keys assert( @@ -179,8 +198,8 @@ type E2e { "unfiltered modules did not include expected fixture module", ) assert( - containsModulePath(allModulePaths, "testdata/go-module-excluded"), - "unfiltered modules did not include skipped fixture module by default", + containsModulePath(allModulePaths, "testdata/go-module-excluded") == false, + "modules included fixture excluded from every operation", ) assert( allModules.get(key: "testdata/go-module-with-testdata").path == "testdata/go-module-with-testdata", @@ -295,38 +314,38 @@ type E2e { "modules includeSkipGenerate false excluded non-generate-skipped module", ) - let normalizedSkipTool = go( + let normalizedSelectionTool = go( version: "1.26.1", - skipLint: ["[\"" + emptyFixturePath + "\"", "\"" + lintFailureFixturePath + "\"]"], - skipTest: ["[\"" + emptyFixturePath + "\"", "\"" + lintFailureFixturePath + "\"]"], - skipGenerate: ["[\"" + emptyFixturePath + "\"", "\"" + lintFailureFixturePath + "\"]"], + lint: ["[\"!" + emptyFixturePath + "\"", "\"!" + lintFailureFixturePath + "\"]"], + test: ["[\"!" + emptyFixturePath + "\"", "\"!" + lintFailureFixturePath + "\"]"], + generate: ["[\"!" + emptyFixturePath + "\"", "\"!" + lintFailureFixturePath + "\"]"], ) assert( - normalizedSkipTool.module(ws, emptyFixturePath).skipLint(ws), - "module did not normalize beta workspace-settings lint skip array fragment", + normalizedSelectionTool.module(ws, emptyFixturePath).skipLint(ws), + "module did not normalize beta workspace-settings lint selector array fragment", ) assert( - normalizedSkipTool.module(ws, lintFailureFixturePath).skipLint(ws), - "module did not normalize trailing beta workspace-settings lint skip array fragment", + normalizedSelectionTool.module(ws, lintFailureFixturePath).skipLint(ws), + "module did not normalize trailing beta workspace-settings lint selector array fragment", ) assert( - normalizedSkipTool.module(ws, "testdata/go-module-with-testdata").skipLint(ws) == false, - "normalized beta workspace-settings lint skip array skipped unrelated module", + normalizedSelectionTool.module(ws, "testdata/go-module-with-testdata").skipLint(ws) == false, + "normalized beta workspace-settings lint selector array skipped unrelated module", ) assert( - normalizedSkipTool.module(ws, emptyFixturePath).skipTest(ws), - "module did not normalize beta workspace-settings test skip array fragment", + normalizedSelectionTool.module(ws, emptyFixturePath).skipTest(ws), + "module did not normalize beta workspace-settings test selector array fragment", ) assert( - normalizedSkipTool.module(ws, lintFailureFixturePath).skipGenerate(ws), - "module did not normalize beta workspace-settings generate skip array fragment", + normalizedSelectionTool.module(ws, lintFailureFixturePath).skipGenerate(ws), + "module did not normalize beta workspace-settings generate selector array fragment", ) - let normalizedLintSkippedModulePaths = normalizedSkipTool + let normalizedLintSkippedModulePaths = normalizedSelectionTool .modules(ws, includeSkipLint: false) .keys assert( containsModulePath(normalizedLintSkippedModulePaths, lintFailureFixturePath) == false, - "normalized beta workspace-settings lint skip array included skipped module", + "normalized beta workspace-settings lint selector array included skipped module", ) let withExtraFiles = go( @@ -523,11 +542,10 @@ type E2e { let skippedTests = tool .module(ws, "testdata/go-module-excluded") .testDirs(ws) - .get(key: "testdata/go-module-excluded") - .tests() + .keys assert( - skippedTests.keys.length == 0, - "tests collection on a skipped directory was not empty", + skippedTests.length == 0, + "testDirs collection on a skipped module was not empty", ) null @@ -543,7 +561,7 @@ type E2e { .layer .sync - let changes = go(version: "1.26.1", skipGenerate: skipGenerate).generateAll(ws) + let changes = go(version: "1.26.1", generate: generate).generateAll(ws) assert( changes.addedPaths.length == 1, @@ -570,7 +588,7 @@ type E2e { let emptyChanges = go( version: "1.26.1", - skipGenerate: [generatedFixturePath], + generate: ["**", "!" + generatedFixturePath], ).module(ws, generatedFixturePath).generate(ws) assert( emptyChanges.addedPaths.length == 0, @@ -601,4 +619,80 @@ type E2e { null } + + """ + Gitignore-style selection: "!" excludes, an include-only list scopes down. + """ + pub selectionCheck(ws: Workspace!): Void @check { + let excluded = go(version: "1.26.1", generate: ["**", "!" + generatedFixturePath]) + assert( + excluded.module(ws, generatedFixturePath).skipGenerate(ws), + "'!' pattern did not exclude its module", + ) + + let includeOnly = go(version: "1.26.1", generate: ["fixtures/**"]) + assert( + includeOnly.module(ws, generatedFixturePath).skipGenerate(ws), + "include-only selection did not exclude an out-of-scope module", + ) + assert( + includeOnly.module(ws, "fixtures/go-module-cross-include-a").skipGenerate(ws) == false, + "include-only selection excluded an in-scope module", + ) + + assert(selectsOut(ws, ["**"], "a/b") == false, "\"**\" did not select a module") + assert(selectsOut(ws, ["*"], "a/b") == false, "\"*\" did not select a module") + + assert(selectsOut(ws, ["**", "!docs"], "docs"), "exclude missed the module itself") + assert(selectsOut(ws, ["**", "!docs"], "docs/snippets"), "exclude missed a nested module") + assert(selectsOut(ws, ["**", "!docs/**"], "docs/snippets"), "\"X/**\" exclude missed a nested module") + assert(selectsOut(ws, ["**", "!docs"], "docs-tools") == false, "exclude leaked across a name-prefix boundary") + + assert(selectsOut(ws, ["!docs"], "docs"), "exclude-only list did not exclude") + assert(selectsOut(ws, ["!docs"], "core") == false, "exclude-only list did not include the rest") + assert(lintSelectsOut(ws, ["!docs"], "docs"), "lint exclude-only list did not exclude") + assert(lintSelectsOut(ws, ["!docs"], "core") == false, "lint exclude-only list did not include the rest") + assert( + lintSelectsOut(ws, ["[\"!docs\"", "\"!tmp\"]"], "docs"), + "lint did not normalize beta workspace-settings array fragments before excluding", + ) + assert( + lintSelectsOut(ws, ["[\"!docs\"", "\"!tmp\"]"], "core") == false, + "lint did not normalize beta workspace-settings array fragments before including the rest", + ) + + let excludeOnlyLintModules = go( + version: "1.26.1", + lint: ["!" + emptyFixturePath], + ).modules(ws, includeSkipLint: false).keys + assert( + containsModulePath(excludeOnlyLintModules, lintFailureFixturePath), + "exclude-only lint settings excluded every non-matching module", + ) + assert( + containsModulePath(excludeOnlyLintModules, emptyFixturePath) == false, + "exclude-only lint settings included the excluded module", + ) + + let scoped = go( + version: "1.26.1", + lint: ["testdata", "!testdata/go-module-skip-tree"], + test: ["testdata", "!testdata/go-module-skip-tree"], + generate: ["testdata/go-module-generate"], + ) + assert( + containsModulePath(scoped.modules(ws).keys, "testdata/go-module-with-testdata"), + "scoped module discovery excluded an included fixture", + ) + assert( + containsModulePath(scoped.modules(ws).keys, "testdata/go-module-skip-tree/nested") == false, + "fully excluded nested module was discovered", + ) + + assert(selectsOut(ws, ["."], "core"), "\".\" leaked to a non-root module") + assert(selectsOut(ws, ["."], ".") == false, "\".\" did not select the root module") + assert(selectsOut(ws, ["!."], "."), "\"!.\" did not exclude the root module") + + null + } } diff --git a/go.dang b/go.dang index 8af3d14..b200a5b 100644 --- a/go.dang +++ b/go.dang @@ -32,17 +32,21 @@ type Go { """ includeExtraFiles: [String!]! = [], """ - Workspace-relative module roots or ancestor subtrees to skip for lint checks. + Module roots to lint. A bare pattern selects, a "!"-prefixed pattern + excludes, and an exclude always wins within this operation. With no + selecting pattern, every module is linted. Supported patterns: "**" or "*" + (all modules), "." (root module only), and "X" or "X/**" (module X and any + module below it). """ - skipLint: [String!]! = [], + lint: [String!]! = ["**"], """ - Workspace-relative module roots or ancestor subtrees to skip for tests. + Module roots to test. Same selection rules as `lint`. """ - skipTest: [String!]! = [], + test: [String!]! = ["**"], """ - Workspace-relative module roots or ancestor subtrees to skip for go generate. + Module roots to run go generate in. Same selection rules as `lint`. """ - skipGenerate: [String!]! = [], + generate: [String!]! = ["**"], ) { if (version != null and base != null) { raise "version and base are mutually exclusive" @@ -50,9 +54,9 @@ type Go { self.version = if (base == null) { version ?? "1.26" } else { null } self.base = base ?? container.from("golang:" + (version ?? "1.26") + "-alpine") self.includeExtraFiles = includeExtraFiles - self.skipLint = normalizePatterns(skipLint) - self.skipTest = normalizePatterns(skipTest) - self.skipGenerate = normalizePatterns(skipGenerate) + self.lint = normalizePatterns(lint) + self.test = normalizePatterns(test) + self.generate = normalizePatterns(generate) self } } @@ -61,7 +65,7 @@ type Go { Normalize selector strings from workspace module settings. Dagger v1.0.0-beta.4 may pass TOML string arrays as JSON-fragment strings - like ["[\"infra\"", "\"docs\"]"]; trim those wrappers so selection still + like ["[\"!infra\"", "\"!docs\"]"]; trim those wrappers so selection still sees the intended patterns. """ let normalizePatterns(patterns: [String!]!): [String!]! { @@ -78,19 +82,19 @@ type Go { pub includeExtraFiles: [String!]! = [] """ - Workspace-relative module roots or ancestor subtrees to skip for lint checks. + Module roots to lint ("!" excludes; see the `lint` constructor argument). """ - pub skipLint: [String!]! = [] + pub lint: [String!]! = ["**"] """ - Workspace-relative module roots or ancestor subtrees to skip for tests. + Module roots to test ("!" excludes; see the `lint` constructor argument). """ - pub skipTest: [String!]! = [] + pub test: [String!]! = ["**"] """ - Workspace-relative module roots or ancestor subtrees to skip for go generate. + Module roots to run go generate in ("!" excludes; see `lint`). """ - pub skipGenerate: [String!]! = [] + pub generate: [String!]! = ["**"] """ Return Go modules discovered from workspace go.mod files, as a collection @@ -110,13 +114,11 @@ type Go { # collection so GoDirectory.tests() can search/filter without ever # touching Workspace again. Functions taking Workspace always invalidate; # operations on a Directory receiver cache by content hash. - let testFilesDir = ws.directory("/", include: ["**/*_test.go"]) - let paths = ws - .directory("/", include: ["**/go.mod"]) - .glob("**/go.mod") - .map { modPath => - if (modPath == "go.mod") { "." } else { modPath.trimSuffix("/go.mod") } - } + let paths = uniqueStrings( + moduleDiscoveryPaths(ws, lint) + + moduleDiscoveryPaths(ws, test) + + moduleDiscoveryPaths(ws, generate), + ) .filter { modPath => modulePathIncluded(ws, modPath, include, exclude) } @@ -126,10 +128,12 @@ type Go { version: version, baseImage: base, includeExtraFiles: includeExtraFiles, - skipLintPaths: skipLint, - skipTestPaths: skipTest, - skipGeneratePaths: skipGenerate, - testFilesDir: testFilesDir, + lintPatterns: lint, + testPatterns: test, + generatePatterns: generate, + includeScanDir: selectedGoFilesDir(ws), + testScanDir: selectedTestScanDir(ws), + testFilesDir: selectedTestFilesDir(ws), ) } .filter { mod => @@ -144,13 +148,157 @@ type Go { version: version, baseImage: base, includeExtraFiles: includeExtraFiles, - skipLintPaths: skipLint, - skipTestPaths: skipTest, - skipGeneratePaths: skipGenerate, - testFilesDir: testFilesDir, + lintPatterns: lint, + testPatterns: test, + generatePatterns: generate, + includeScanDir: selectedGoFilesDir(ws), + testScanDir: selectedTestScanDir(ws), + testFilesDir: selectedTestFilesDir(ws), + ) + } + + let uniqueStrings(values: [String!]!): [String!]! { + values.reduce([]) { acc, value => + if (acc.any { existing => existing == value }) { + acc + } else { + acc + [value] + } + } + } + + let moduleDiscoveryPaths(ws: Workspace!, patterns: [String!]!): [String!]! { + ws + .directory( + "/", + include: selectorGoModIncludes(patterns), + exclude: selectorGoModExcludes(patterns), + ) + .glob("**/go.mod") + .map { modPath => + if (modPath == "go.mod") { "." } else { modPath.trimSuffix("/go.mod") } + } + .filter { modPath => moduleSelected(patterns, modPath) } + } + + let selectedTestFilesDir(ws: Workspace!): Directory! { + ws.directory( + "/", + include: selectorTestFileIncludes(test), + exclude: selectorTestFileExcludes(test), + ) + } + + let selectedGoFilesDir(ws: Workspace!): Directory! { + ws.directory( + "/", + include: uniqueStrings( + selectorGoFileIncludes(lint) + + selectorGoFileIncludes(test) + + selectorGoFileIncludes(generate) + + selectorGoModIncludes(lint) + + selectorGoModIncludes(test) + + selectorGoModIncludes(generate), + ), + exclude: uniqueStrings( + selectorGoFileExcludes(lint) + + selectorGoFileExcludes(test) + + selectorGoFileExcludes(generate) + + selectorGoModExcludes(lint) + + selectorGoModExcludes(test) + + selectorGoModExcludes(generate), + ), + ) + } + + let selectedTestScanDir(ws: Workspace!): Directory! { + ws.directory( + "/", + include: uniqueStrings( + selectorGoFileIncludes(test) + + selectorGoModIncludes(test), + ), + exclude: uniqueStrings( + selectorGoFileExcludes(test) + + selectorGoModExcludes(test), + ), ) } + let selectorGoModIncludes(patterns: [String!]!): [String!]! { + let included = patterns.filter { p => p.hasPrefix("!") == false } + if (included.length == 0) { + ["**/go.mod"] + } else { + included.map { p => selectorFileGlob(p, "go.mod") } + } + } + + let selectorGoModExcludes(patterns: [String!]!): [String!]! { + patterns + .filter { p => p.hasPrefix("!") } + .map { p => selectorFileGlob(p.trimPrefix("!"), "go.mod") } + } + + let selectorGoFileIncludes(patterns: [String!]!): [String!]! { + let included = patterns.filter { p => p.hasPrefix("!") == false } + if (included.length == 0) { + ["**/*.go"] + } else { + included.map { p => selectorFileGlob(p, "*.go") } + } + } + + let selectorGoFileExcludes(patterns: [String!]!): [String!]! { + patterns + .filter { p => p.hasPrefix("!") } + .map { p => selectorFileGlob(p.trimPrefix("!"), "*.go") } + } + + let selectorTestFileIncludes(patterns: [String!]!): [String!]! { + let included = patterns.filter { p => p.hasPrefix("!") == false } + if (included.length == 0) { + ["**/*_test.go"] + } else { + included.map { p => selectorFileGlob(p, "*_test.go") } + } + } + + let selectorTestFileExcludes(patterns: [String!]!): [String!]! { + patterns + .filter { p => p.hasPrefix("!") } + .map { p => selectorFileGlob(p.trimPrefix("!"), "*_test.go") } + } + + let selectorFileGlob(pattern: String!, filePattern: String!): String! { + let p = pattern.trimPrefix("/").trimSuffix("/") + if (p == "**" or p == "*") { + "**/" + filePattern + } else if (p == ".") { + filePattern + } else { + p.trimSuffix("/**") + "/**/" + filePattern + } + } + + let moduleSelected(patterns: [String!]!, modPath: String!): Boolean! { + let included = patterns.filter { p => p.hasPrefix("!") == false } + let excluded = patterns.filter { p => p.hasPrefix("!") }.map { p => p.trimPrefix("!") } + let isIncluded = included.length == 0 or included.any { p => matchesModulePath(modPath, p) } + let isExcluded = excluded.any { p => matchesModulePath(modPath, p) } + isIncluded and isExcluded == false + } + + let matchesModulePath(modPath: String!, pattern: String!): Boolean! { + let p = pattern.trimPrefix("/").trimSuffix("/") + if (p == "**" or p == "*") { + true + } else { + let prefix = p.trimSuffix("/**") + modPath == prefix or modPath.hasPrefix(prefix + "/") + } + } + let modulePathIncluded( ws: Workspace!, modPath: String!, @@ -193,10 +341,12 @@ type Go { version: version, baseImage: base, includeExtraFiles: includeExtraFiles, - skipLintPaths: skipLint, - skipTestPaths: skipTest, - skipGeneratePaths: skipGenerate, - testFilesDir: ws.directory("/", include: ["**/*_test.go"]), + lintPatterns: lint, + testPatterns: test, + generatePatterns: generate, + includeScanDir: selectedGoFilesDir(ws), + testScanDir: selectedTestScanDir(ws), + testFilesDir: selectedTestFilesDir(ws), ) } @@ -281,9 +431,11 @@ type GoModules { let version: String let baseImage: Container! let includeExtraFiles: [String!]! - let skipLintPaths: [String!]! - let skipTestPaths: [String!]! - let skipGeneratePaths: [String!]! + let lintPatterns: [String!]! + let testPatterns: [String!]! + let generatePatterns: [String!]! + let includeScanDir: Directory! + let testScanDir: Directory! let testFilesDir: Directory! """ @@ -295,9 +447,11 @@ type GoModules { version: version, baseImage: baseImage, includeExtraFiles: includeExtraFiles, - skipLintPaths: skipLintPaths, - skipTestPaths: skipTestPaths, - skipGeneratePaths: skipGeneratePaths, + lintPatterns: lintPatterns, + testPatterns: testPatterns, + generatePatterns: generatePatterns, + includeScanDir: includeScanDir, + testScanDir: testScanDir, testFilesDir: testFilesDir, ) } @@ -328,19 +482,29 @@ type GoModule { pub includeExtraFiles: [String!]! """ - Workspace-relative module roots or ancestor subtrees to skip for lint checks. + Gitignore-style globs selecting which module roots to lint. """ - let skipLintPaths: [String!]! + let lintPatterns: [String!]! """ - Workspace-relative module roots or ancestor subtrees to skip for tests. + Gitignore-style globs selecting which module roots to test. """ - let skipTestPaths: [String!]! + let testPatterns: [String!]! """ - Workspace-relative module roots or ancestor subtrees to skip for go generate. + Gitignore-style globs selecting which module roots to run go generate in. """ - let skipGeneratePaths: [String!]! + let generatePatterns: [String!]! + + """ + Workspace snapshot used by the local include helper. + """ + let includeScanDir: Directory! + + """ + Workspace snapshot used by the local test-directory helper. + """ + let testScanDir: Directory! """ Workspace-wide snapshot of every *_test.go file, threaded down from the @@ -383,25 +547,56 @@ type GoModule { .withEnvVariable("DAGGER_GO_WORKSPACE_ID", toJSON(ws.id)) } + let moduleOutputFile: String! { + (if (path == ".") { "_root_" } else { path }) + ".inc" + } + """ - Whether this module or an ancestor subtree is configured to skip lint. + Base container for local helper passes over a scoped workspace snapshot. + """ + let localIncludesHelper(scanDir: Directory!): Container! { + baseImage + .withoutEntrypoint + .withWorkdir("/helpers/go-includes") + .withMountedCache("/go/pkg/mod", cacheVolume("go-mod")) + .withMountedCache("/root/.cache/go-build", cacheVolume("go-build")) + .withDirectory("/helpers/go-includes", currentModule.source.directory("helpers/go-includes")) + .withExec(["go", "build", "-o", "/usr/local/bin/go-includes", "."]) + .withDirectory("/ws", scanDir) + .withWorkdir("/ws") + } + + let allIncludesDir(lint: Boolean!, test: Boolean!, generate: Boolean!): Directory! { + localIncludesHelper(includeScanDir) + .withExec(["go-includes", "--all", "--output-dir", "/output"] + includeFlags(lint, test, generate)) + .directory("/output") + } + + let allTestDirsDir: Directory! { + localIncludesHelper(testScanDir) + .withExec(["go-includes", "--all", "--test-dirs", "--output-dir", "/output"]) + .directory("/output") + } + + """ + Whether this module falls outside the configured lint selection. """ pub skipLint(ws: Workspace!): Boolean! { - hasSkipPath(skipLintPaths) + outOfScope(lintPatterns) } """ - Whether this module or an ancestor subtree is configured to skip tests. + Whether this module falls outside the configured test selection. """ pub skipTest(ws: Workspace!): Boolean! { - hasSkipPath(skipTestPaths) + outOfScope(testPatterns) } """ - Whether this module or an ancestor subtree is configured to skip go generate. + Whether this module falls outside the configured go generate selection. """ pub skipGenerate(ws: Workspace!): Boolean! { - hasSkipPath(skipGeneratePaths) + outOfScope(generatePatterns) } """ @@ -431,12 +626,33 @@ type GoModule { } """ - Whether this module root is at or below a configured skip path. + Whether this module root falls outside the selection `patterns`. + + Matching is on the module root path only: no workspace files are read. A + bare pattern includes; a "!"-prefixed pattern excludes. With no include + pattern, every module is included. A module is in scope when it matches an + include and no exclude; an exclude always wins within that operation. + """ + let outOfScope(patterns: [String!]!): Boolean! { + let included = patterns.filter { p => p.hasPrefix("!") == false } + let excluded = patterns.filter { p => p.hasPrefix("!") }.map { p => p.trimPrefix("!") } + let isIncluded = included.length == 0 or included.any { p => matchesModulePath(p) } + let isExcluded = excluded.any { p => matchesModulePath(p) } + isIncluded == false or isExcluded + } + + """ + Whether this module root matches a single glob: "**"/"*" match everything, + "." matches only the root module, and "X/**" or bare "X" match X and any + module below it. """ - let hasSkipPath(skipPaths: [String!]!): Boolean! { - skipPaths.any { skipPath => - let clean = skipPath.trimPrefix("/").trimSuffix("/") - clean == "." or clean == "" or path == clean or path.hasPrefix(clean + "/") + let matchesModulePath(pattern: String!): Boolean! { + let p = pattern.trimPrefix("/").trimSuffix("/") + if (p == "**" or p == "*") { + true + } else { + let prefix = p.trimSuffix("/**") + path == prefix or path.hasPrefix(prefix + "/") } } @@ -451,12 +667,8 @@ type GoModule { test: Boolean! = false, generate: Boolean! = false, ): [String!]! { - goIncludesHelper(ws) - .withExec( - ["go-includes", "--output", "/output"] + includeFlags(lint, test, generate) + [workspacePath], - experimentalPrivilegedNesting: true, - ) - .file("/output") + allIncludesDir(lint: lint, test: test, generate: generate) + .file(moduleOutputFile) .contents .split("\n") .filter { pattern => pattern != "" } @@ -473,22 +685,24 @@ type GoModule { modulePath: path, baseImage: baseImage, includeExtraFiles: includeExtraFiles, - skipTestPaths: skipTestPaths, + testPatterns: testPatterns, + includeScanDir: includeScanDir, + testScanDir: testScanDir, testFilesDir: testFilesDir, ) } } let testDirectoryPaths(ws: Workspace!): [String!]! { - goIncludesHelper(ws) - .withExec( - ["go-includes", "--output", "/output", "--test-dirs", workspacePath], - experimentalPrivilegedNesting: true, - ) - .file("/output") - .contents - .split("\n") - .filter { testPath => testPath != "" } + if (skipTest(ws)) { + [] + } else { + allTestDirsDir + .file(moduleOutputFile) + .contents + .split("\n") + .filter { testPath => testPath != "" } + } } """ @@ -501,7 +715,9 @@ type GoModule { modulePath: path, baseImage: baseImage, includeExtraFiles: includeExtraFiles, - skipTestPaths: skipTestPaths, + testPatterns: testPatterns, + includeScanDir: includeScanDir, + testScanDir: testScanDir, testFilesDir: testFilesDir, ) } @@ -720,7 +936,9 @@ type GoTestDirs { let modulePath: String! let baseImage: Container! let includeExtraFiles: [String!]! - let skipTestPaths: [String!]! + let testPatterns: [String!]! + let includeScanDir: Directory! + let testScanDir: Directory! let testFilesDir: Directory! """ @@ -733,7 +951,9 @@ type GoTestDirs { modulePath: modulePath, baseImage: baseImage, includeExtraFiles: includeExtraFiles, - skipTestPaths: skipTestPaths, + testPatterns: testPatterns, + includeScanDir: includeScanDir, + testScanDir: testScanDir, testFilesDir: testFilesDir, ) } @@ -766,9 +986,19 @@ type GoDirectory { let baseImage: Container! """ - Workspace-relative module roots or ancestor subtrees to skip for tests. + Gitignore-style globs selecting which module roots to test. + """ + let testPatterns: [String!]! + + """ + Workspace snapshot used by the local include helper. + """ + let includeScanDir: Directory! + + """ + Workspace snapshot used by the local test-directory helper. """ - let skipTestPaths: [String!]! + let testScanDir: Directory! """ Workspace-wide snapshot of every *_test.go file, threaded down from the @@ -811,13 +1041,58 @@ type GoDirectory { .withEnvVariable("DAGGER_GO_WORKSPACE_ID", toJSON(ws.id)) } + let moduleOutputFile: String! { + (if (modulePath == ".") { "_root_" } else { modulePath }) + ".inc" + } + + """ + Base container for local helper passes over a scoped workspace snapshot. + """ + let localIncludesHelper(scanDir: Directory!): Container! { + baseImage + .withoutEntrypoint + .withWorkdir("/helpers/go-includes") + .withMountedCache("/go/pkg/mod", cacheVolume("go-mod")) + .withMountedCache("/root/.cache/go-build", cacheVolume("go-build")) + .withDirectory("/helpers/go-includes", currentModule.source.directory("helpers/go-includes")) + .withExec(["go", "build", "-o", "/usr/local/bin/go-includes", "."]) + .withDirectory("/ws", scanDir) + .withWorkdir("/ws") + } + + let allIncludesDir: Directory! { + localIncludesHelper(includeScanDir) + .withExec(["go-includes", "--all", "--test", "--output-dir", "/output"]) + .directory("/output") + } + """ - Whether this directory's module is at or below a configured test skip path. + Whether this directory's module falls outside the configured test selection. + + Matching is on the module root path only: no workspace files are read. A + bare pattern includes; a "!"-prefixed pattern excludes; an exclude always + wins. With no include pattern, every module is included. """ let skipTest: Boolean! { - skipTestPaths.any { skipPath => - let clean = skipPath.trimPrefix("/").trimSuffix("/") - clean == "." or clean == "" or modulePath == clean or modulePath.hasPrefix(clean + "/") + let included = testPatterns.filter { p => p.hasPrefix("!") == false } + let excluded = testPatterns.filter { p => p.hasPrefix("!") }.map { p => p.trimPrefix("!") } + let isIncluded = included.length == 0 or included.any { p => matchesModulePath(p) } + let isExcluded = excluded.any { p => matchesModulePath(p) } + isIncluded == false or isExcluded + } + + """ + Whether this directory's module root matches a single glob: "**"/"*" match + everything, "." matches only the root module, and "X/**" or bare "X" match X + and any module below it. + """ + let matchesModulePath(pattern: String!): Boolean! { + let p = pattern.trimPrefix("/").trimSuffix("/") + if (p == "**" or p == "*") { + true + } else { + let prefix = p.trimSuffix("/**") + modulePath == prefix or modulePath.hasPrefix(prefix + "/") } } @@ -825,12 +1100,8 @@ type GoDirectory { Additional workspace include patterns discovered for this directory's Go tests. """ let includeDiscovered: [String!]! { - goIncludesHelper - .withExec( - ["go-includes", "--output", "/output", "--test", workspacePath], - experimentalPrivilegedNesting: true, - ) - .file("/output") + allIncludesDir + .file(moduleOutputFile) .contents .split("\n") .filter { pattern => pattern != "" } diff --git a/helpers/go-includes/all.go b/helpers/go-includes/all.go new file mode 100644 index 0000000..64b0692 --- /dev/null +++ b/helpers/go-includes/all.go @@ -0,0 +1,301 @@ +package main + +import ( + "flag" + "fmt" + "io/fs" + "os" + "path" + "path/filepath" + "sort" + "strings" + + "golang.org/x/mod/modfile" +) + +// runAll computes results for every module in one local pass over a mounted +// workspace snapshot (default: the current directory). It never touches the +// workspace gateway; all reads are os.ReadFile. +func runAll(cliArgs []string) error { + flags := flag.NewFlagSet("go-includes --all", flag.ExitOnError) + flags.Bool("all", false, "compute results for every module") + lint := flags.Bool("lint", false, "include lint inputs") + test := flags.Bool("test", false, "include test inputs") + generate := flags.Bool("generate", false, "include generate inputs") + testDirs := flags.Bool("test-dirs", false, "write directories containing Go tests") + root := flags.String("root", ".", "workspace root to scan") + outputDir := flags.String("output-dir", "", "directory to write one file per module to") + if err := flags.Parse(cliArgs); err != nil { + return err + } + if *outputDir == "" { + return fmt.Errorf("--output-dir is required") + } + if !*lint && !*test && !*generate && !*testDirs { + *test = true + } + + index, err := indexLocal(*root) + if err != nil { + return err + } + if *testDirs { + return index.writeAllTestDirsDir(*outputDir) + } + return index.writeAllIncludesDir(*outputDir, *lint, *test, *generate) +} + +// moduleOutputFile returns the per-module output filename for a module root. +// The ".inc" suffix keeps a module's file distinct from a nested module's +// subdirectory (e.g. "sdk/go.inc" never collides with the "sdk/go/" tree). +func moduleOutputFile(moduleRoot string) string { + name := moduleRoot + if moduleRoot == "." { + name = "_root_" + } + return filepath.FromSlash(name) + ".inc" +} + +func writeLines(filePath string, lines []string) error { + if err := os.MkdirAll(filepath.Dir(filePath), 0o755); err != nil { + return err + } + data := "" + if len(lines) > 0 { + data = strings.Join(lines, "\n") + "\n" + } + return os.WriteFile(filePath, []byte(data), 0o644) +} + +// writeAllIncludesDir writes one include-pattern file per module, so each +// consumer reads only its own slice instead of re-scanning a combined blob. +func (index *localIndex) writeAllIncludesDir(dir string, lint, test, generate bool) error { + for _, moduleRoot := range index.moduleRoots { + includes, err := index.includesFor(moduleRoot, lint, test, generate) + if err != nil { + return err + } + if err := writeLines(filepath.Join(dir, moduleOutputFile(moduleRoot)), includes); err != nil { + return err + } + } + return nil +} + +// writeAllTestDirsDir writes one test-directory file per module. +func (index *localIndex) writeAllTestDirsDir(dir string) error { + for _, moduleRoot := range index.moduleRoots { + if err := writeLines(filepath.Join(dir, moduleOutputFile(moduleRoot)), index.testDirectoriesFor(moduleRoot)); err != nil { + return err + } + } + return nil +} + +// localIndex holds a workspace snapshot indexed for local include computation. +type localIndex struct { + root string + moduleRoots []string + moduleSet map[string]bool + goFilesByModule map[string][]string +} + +// indexLocal walks the snapshot once, recording modules and their Go files. +func indexLocal(root string) (*localIndex, error) { + index := &localIndex{ + root: root, + moduleSet: map[string]bool{}, + goFilesByModule: map[string][]string{}, + } + var goMods, goFiles []string + err := filepath.WalkDir(root, func(p string, d fs.DirEntry, err error) error { + if err != nil { + return err + } + if d.IsDir() { + return nil + } + rel, err := filepath.Rel(root, p) + if err != nil { + return err + } + rel = filepath.ToSlash(rel) + switch { + case d.Name() == "go.mod": + goMods = append(goMods, rel) + case strings.HasSuffix(d.Name(), ".go"): + goFiles = append(goFiles, rel) + } + return nil + }) + if err != nil { + return nil, err + } + + sort.Strings(goMods) + for _, goModPath := range goMods { + moduleRoot := strings.TrimSuffix(goModPath, "/go.mod") + if goModPath == "go.mod" { + moduleRoot = "." + } + index.moduleRoots = append(index.moduleRoots, moduleRoot) + index.moduleSet[moduleRoot] = true + } + + for _, goFile := range goFiles { + moduleRoot, ok := containingModuleDir(index.moduleSet, path.Dir(goFile)) + if !ok { + continue + } + index.goFilesByModule[moduleRoot] = append(index.goFilesByModule[moduleRoot], goFile) + } + return index, nil +} + +// includesFor mirrors targetModule.includes using local file reads. +func (index *localIndex) includesFor(moduleRoot string, lint, test, generate bool) ([]string, error) { + queued := map[string]bool{moduleRoot: true} + queue := []string{moduleRoot} + var includes []string + + for len(queue) > 0 { + module := queue[0] + queue = queue[1:] + + directives, err := index.directives(module) + if err != nil { + return nil, err + } + + includes = append(includes, includeBasePatterns(module)...) + for _, directive := range directives { + switch { + case directive.isEmbed(): + case generate && directive.isGenerateInclude(): + case test && directive.isTestInclude(): + default: + continue + } + patterns, err := directive.includePatterns() + if err != nil { + return nil, err + } + includes = append(includes, patterns...) + } + + next, err := index.replaceModules(module) + if err != nil { + return nil, err + } + if generate { + generateModules, err := index.generateModules(directives) + if err != nil { + return nil, err + } + next = append(next, generateModules...) + } + for _, nextModule := range next { + if !queued[nextModule] { + queued[nextModule] = true + queue = append(queue, nextModule) + } + } + } + + deduped := make([]string, 0, len(includes)) + seen := map[string]bool{} + for _, include := range includes { + if seen[include] { + continue + } + seen[include] = true + deduped = append(deduped, include) + } + return deduped, nil +} + +// testDirectoriesFor returns module directories containing at least one *_test.go file. +func (index *localIndex) testDirectoriesFor(moduleRoot string) []string { + dirs := make([]string, 0, len(index.goFilesByModule[moduleRoot])) + seen := map[string]bool{} + for _, goFile := range index.goFilesByModule[moduleRoot] { + if !strings.HasSuffix(goFile, "_test.go") { + continue + } + dir := path.Dir(goFile) + if seen[dir] { + continue + } + seen[dir] = true + dirs = append(dirs, dir) + } + sort.Strings(dirs) + return dirs +} + +// directives parses every Go file belonging to a module root. +func (index *localIndex) directives(moduleRoot string) ([]goDirective, error) { + files := index.goFilesByModule[moduleRoot] + sort.Strings(files) + + var directives []goDirective + for _, filePath := range files { + data, err := os.ReadFile(filepath.Join(index.root, filepath.FromSlash(filePath))) + if err != nil { + return nil, err + } + fileDirectives, err := goDirectivesInFile(filePath, data) + if err != nil { + return nil, err + } + directives = append(directives, fileDirectives...) + } + return directives, nil +} + +// replaceModules resolves local go.mod replace targets to module roots. +func (index *localIndex) replaceModules(moduleRoot string) ([]string, error) { + goModPath := path.Join(moduleRoot, "go.mod") + data, err := os.ReadFile(filepath.Join(index.root, filepath.FromSlash(goModPath))) + if err != nil { + return nil, err + } + goMod, err := modfile.Parse(goModPath, data, nil) + if err != nil { + return nil, err + } + + var roots []string + for _, replace := range goMod.Replace { + if replace.New.Version != "" || (!strings.HasPrefix(replace.New.Path, "./") && !strings.HasPrefix(replace.New.Path, "../")) { + continue + } + target := strings.TrimSuffix(replace.New.Path, "/") + root, ok := containingModuleDir(index.moduleSet, path.Join(path.Dir(goModPath), target)) + if !ok { + return nil, fmt.Errorf("no Go module found for local replace target: %s", replace.New.Path) + } + roots = append(roots, root) + } + return roots, nil +} + +// generateModules resolves go:generate go -C targets to module roots. +func (index *localIndex) generateModules(directives []goDirective) ([]string, error) { + var roots []string + for _, directive := range directives { + workdir, ok, err := directive.generateGoDashC() + if err != nil { + return nil, err + } + if !ok { + continue + } + root, ok := containingModuleDir(index.moduleSet, path.Join(directive.dir(), workdir)) + if !ok { + return nil, fmt.Errorf("%s: no Go module found for go -C directory: %s", directive.position, workdir) + } + roots = append(roots, root) + } + return roots, nil +} diff --git a/helpers/go-includes/main.go b/helpers/go-includes/main.go index 6d4c89d..c1e39a9 100644 --- a/helpers/go-includes/main.go +++ b/helpers/go-includes/main.go @@ -29,6 +29,18 @@ func main() { ctx := telemetry.Init(context.Background(), telemetry.Config{Detect: true}) defer telemetry.Close() + // --all computes results for every module in one local pass over a mounted + // workspace snapshot, with no workspace gateway round-trips. + for _, arg := range os.Args[1:] { + if arg == "--all" { + if err := runAll(os.Args[1:]); err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } + return + } + } + targetModule, testDirs, outputPath, err := newTargetModuleFromArgs(ctx, os.Args[1:]) if err != nil { fmt.Fprintln(os.Stderr, err) @@ -166,9 +178,14 @@ func (w *workspace) indexModules(ctx context.Context) error { // containingModuleDir finds the nearest ancestor module root for a workspace path. func (w *workspace) containingModuleDir(dir string) (string, bool) { + return containingModuleDir(w.moduleSet, dir) +} + +// containingModuleDir finds the nearest ancestor module root in moduleSet. +func containingModuleDir(moduleSet map[string]bool, dir string) (string, bool) { dir = path.Clean(strings.TrimPrefix(dir, "/")) for { - if w.moduleSet[dir] { + if moduleSet[dir] { return dir, true } if dir == "." { @@ -353,10 +370,21 @@ func readRegularFile(ctx context.Context, dir *dagger.Directory, filePath string // includeBase returns the static Go source patterns for this module root. func (t targetModule) includeBase() []string { + return includeBasePatterns(t.moduleRoot) +} + +// includeBasePatterns returns the static Go source patterns for a module root. +func includeBasePatterns(moduleRoot string) []string { patterns := []string{ "**/*.go", "**/*.c", + "**/*.cc", + "**/*.cpp", + "**/*.cxx", "**/*.h", + "**/*.hh", + "**/*.hpp", + "**/*.hxx", "**/*.s", "**/*.S", "**/*.syso", @@ -370,7 +398,7 @@ func (t targetModule) includeBase() []string { "go.work.sum", } for i, pattern := range patterns { - patterns[i] = t.subpath(pattern) + patterns[i] = path.Join(moduleRoot, pattern) } return patterns } diff --git a/helpers/go-includes/main_test.go b/helpers/go-includes/main_test.go index c642e9f..7bb8699 100644 --- a/helpers/go-includes/main_test.go +++ b/helpers/go-includes/main_test.go @@ -106,7 +106,13 @@ func TestIncludeBasePreservesNestedModuleBoundaries(t *testing.T) { want := []string{ "pkg/**/*.go", "pkg/**/*.c", + "pkg/**/*.cc", + "pkg/**/*.cpp", + "pkg/**/*.cxx", "pkg/**/*.h", + "pkg/**/*.hh", + "pkg/**/*.hpp", + "pkg/**/*.hxx", "pkg/**/*.s", "pkg/**/*.S", "pkg/**/*.syso",