diff --git a/.dagger/modules/e2e/dagger.json b/.dagger/modules/e2e/dagger.json new file mode 100644 index 0000000..054a87f --- /dev/null +++ b/.dagger/modules/e2e/dagger.json @@ -0,0 +1,13 @@ +{ + "name": "e2e", + "engineVersion": "v1.0.0-beta.7", + "sdk": { + "source": "dang" + }, + "dependencies": [ + { + "name": "go", + "source": "../../.." + } + ] +} diff --git a/.dagger/modules/e2e/main.dang b/.dagger/modules/e2e/main.dang index 97a9db5..5b0d50f 100644 --- a/.dagger/modules/e2e/main.dang +++ b/.dagger/modules/e2e/main.dang @@ -7,17 +7,23 @@ 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 lint: [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", ] @@ -63,6 +69,14 @@ type E2e { 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) + } + """ Module discovery (findConfigDirs) returns exactly the directories holding a go.mod, at any depth, and nothing else. @@ -124,6 +138,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. """ @@ -571,6 +592,43 @@ type E2e { # No includes means "everything", minus excludes. 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).{path} + 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 lintFailure = try { + go( + version: "1.26.1", + lint: ["!" + emptyFixturePath], + ).module(ws, lintFailureFixturePath).lint(ws) + "lint unexpectedly succeeded" + } catch { + err => err.message + } + assert( + lintFailure.contains("exit code"), + "exclude-only lint settings did not run lint on a non-excluded module: " + lintFailure, + ) # "." selects only the root module. assert(selectsOut(ws, ["."], "core"), "\".\" leaked to a non-root module") diff --git a/dagger.json b/dagger.json index 9b5dc34..0ac60ba 100644 --- a/dagger.json +++ b/dagger.json @@ -1,6 +1,6 @@ { "name": "go", - "engineVersion": "v0.20.6", + "engineVersion": "v1.0.0-beta.7", "sdk": { "source": "dang" }, diff --git a/go.dang b/go.dang index 7565d07..f4f2783 100644 --- a/go.dang +++ b/go.dang @@ -56,13 +56,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.lint = lint - self.test = test - self.generate = generate + self.lint = normalizePatterns(lint) + self.test = normalizePatterns(test) + self.generate = normalizePatterns(generate) 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 @@ -292,29 +305,6 @@ type GoModule { } } - """ - Absolute workspace path for this module root. - """ - let workspacePath: String! { - if (path == ".") { "/" } else { "/" + path.trimSuffix("/") } - } - - """ - Base container for the Go include helper. - """ - let goIncludesHelper(ws: Workspace!): 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", directory) - .withWorkdir("/ws") - .withEnvVariable("DAGGER_GO_WORKSPACE_ID", toJSON(ws.id)) - } - """ Whether this module falls outside the configured lint selection. """ @@ -350,7 +340,7 @@ type GoModule { literal: true, filesOnly: true, limit: 1, - ).{id} + ).{{id}} .length > 0 } } @@ -412,10 +402,9 @@ type GoModule { } """ - Discovery output directory: one file of include patterns per module, named - ".inc" (the root module is "_root_.inc"). For a given mode every - module produces the same container and exec, so discovery runs once per - workspace rather than once per module, and each module reads only its own file. + Discovery output directory: include patterns and test directories for every + module. For a given mode every module produces the same container and exec, + so discovery runs once per workspace and each module reads only its own files. """ let allIncludesDir( ws: Workspace!, @@ -451,12 +440,15 @@ type GoModule { Directories in this module containing Go test files. """ pub testDirectories(ws: Workspace!): [GoDirectory!]! { - goIncludesHelper(ws) - .withExec( - ["go-includes", "--output", "/output", "--test-dirs", workspacePath], - experimentalPrivilegedNesting: true, - ) - .file("/output") + let outputStem = if (path == ".") { "_root_" } else { path } + let index = allIncludesDir(ws, lint: false, test: true, generate: false) + let discoveredIncludes = index + .file(outputStem + ".inc") + .contents + .split("\n") + .filter { pattern => pattern != "" } + index + .file(outputStem + ".testdirs") .contents .split("\n") .filter { testPath => testPath != "" } @@ -467,6 +459,7 @@ type GoModule { modulePath: path, baseImage: baseImage, includeExtraFiles: includeExtraFiles, + includeDiscovered: discoveredIncludes, testPatterns: testPatterns, ) } @@ -489,7 +482,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"), @@ -621,6 +620,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")) @@ -686,6 +687,11 @@ type GoDirectory { """ let includeExtraFiles: [String!]! + """ + Additional workspace inputs discovered for this directory's Go module. + """ + let includeDiscovered: [String!]! + """ Base image used by this directory's Go containers. """ @@ -707,29 +713,6 @@ type GoDirectory { } } - """ - Absolute workspace path for this directory's Go module root. - """ - let workspacePath: String! { - if (modulePath == ".") { "/" } else { "/" + modulePath.trimSuffix("/") } - } - - """ - Base container for the Go include helper. - """ - let goIncludesHelper: 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", directory) - .withWorkdir("/ws") - .withEnvVariable("DAGGER_GO_WORKSPACE_ID", toJSON(ws.id)) - } - """ Whether this directory's module falls outside the configured test selection. @@ -759,21 +742,6 @@ 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") - .contents - .split("\n") - .filter { pattern => pattern != "" } - } - """ Final workspace include patterns used to build this directory's source. """ diff --git a/helpers/go-includes/all.go b/helpers/go-includes/all.go index 3715a85..3bd9422 100644 --- a/helpers/go-includes/all.go +++ b/helpers/go-includes/all.go @@ -46,11 +46,20 @@ func runAll(cliArgs []string) error { // 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 moduleIncludeFile(moduleRoot string) string { + return moduleOutputFile(moduleRoot, ".inc") +} + +// moduleTestDirectoriesFile returns the per-module test-directory output file. +func moduleTestDirectoriesFile(moduleRoot string) string { + return moduleOutputFile(moduleRoot, ".testdirs") +} + +func moduleOutputFile(moduleRoot, suffix string) string { name := moduleRoot if moduleRoot == "." { name = "_root_" } - return filepath.FromSlash(name) + ".inc" + return filepath.FromSlash(name) + suffix } // writeAllDir writes one file of include patterns per module, so each consumer @@ -69,6 +78,16 @@ func (index *localIndex) writeAllDir(dir string, lint, test, generate bool) erro if err := os.WriteFile(outPath, []byte(data), 0o644); err != nil { return err } + + testDirs := index.testDirectoriesFor(moduleRoot) + testDirsData := strings.Join(testDirs, "\n") + if len(testDirs) > 0 { + testDirsData += "\n" + } + testDirsPath := filepath.Join(dir, moduleTestDirectoriesFile(moduleRoot)) + if err := os.WriteFile(testDirsPath, []byte(testDirsData), 0o644); err != nil { + return err + } } return nil } @@ -215,6 +234,16 @@ func (index *localIndex) directives(moduleRoot string) ([]goDirective, error) { return directives, nil } +func (index *localIndex) testDirectoriesFor(moduleRoot string) []string { + var testFiles []string + for _, filePath := range index.goFilesByModule[moduleRoot] { + if strings.HasSuffix(filePath, "_test.go") { + testFiles = append(testFiles, filePath) + } + } + return testDirectoriesFromFiles(testFiles) +} + // replaceModules resolves local go.mod replace targets to module roots. func (index *localIndex) replaceModules(moduleRoot string) ([]string, error) { goModPath := path.Join(moduleRoot, "go.mod") diff --git a/helpers/go-includes/main_test.go b/helpers/go-includes/main_test.go index 7b4ba63..b7910a9 100644 --- a/helpers/go-includes/main_test.go +++ b/helpers/go-includes/main_test.go @@ -172,6 +172,21 @@ func TestTestDirectoriesFromFiles(t *testing.T) { } } +func TestLocalIndexTestDirectories(t *testing.T) { + index := &localIndex{goFilesByModule: map[string][]string{ + "api": { + "api/auth/auth.go", + "api/auth/auth_test.go", + "api/auth/more_test.go", + "api/db/db_test.go", + }, + }} + want := []string{"api/auth", "api/db"} + if got := index.testDirectoriesFor("api"); !reflect.DeepEqual(got, want) { + t.Fatalf("testDirectoriesFor mismatch:\n got: %#v\nwant: %#v", got, want) + } +} + func TestInvalidQuotedDirectiveArg(t *testing.T) { _, err := (goDirective{ position: "test.go:1:1", diff --git a/helpers/go-includes/workspace_shim.go b/helpers/go-includes/workspace_shim.go index 085155b..7e867fc 100644 --- a/helpers/go-includes/workspace_shim.go +++ b/helpers/go-includes/workspace_shim.go @@ -2,30 +2,17 @@ package main import ( "context" - "encoding/json" - "fmt" "os" "dagger.io/dagger" ) -const workspaceIDEnv = "DAGGER_GO_WORKSPACE_ID" - -// currentWorkspace is a temporary shim for dag.CurrentWorkspace, which exists -// but is not usable here yet. +// currentWorkspace supports the helper's legacy gateway mode. Module calls use +// the local snapshot mode and do not depend on an ambient workspace. func currentWorkspace(ctx context.Context) (*dagger.Workspace, error) { - rawID := os.Getenv(workspaceIDEnv) - if rawID == "" { - return nil, fmt.Errorf("%s is not set", workspaceIDEnv) - } - workspaceID := rawID - if err := json.Unmarshal([]byte(rawID), &workspaceID); err != nil { - workspaceID = rawID - } - client, err := dagger.Connect(ctx, dagger.WithLogOutput(os.Stderr)) if err != nil { return nil, err } - return dagger.Ref[*dagger.Workspace](client, dagger.ID(workspaceID)), nil + return client.CurrentWorkspace(), nil } 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)