Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions .github/workflows/go.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -93,6 +93,20 @@ jobs:
with:
config: ./.testcoverage.yaml

# The CGO-free companion/self-cross build (`-tags codefly_nosemantic`,
# CGO_ENABLED=0) selects pkg/engine/source_nosemantic.go. `codefly
# companion publish` exercises it only in companions.yaml, which needs
# Docker; verify here, unconditionally, that the variant both links
# statically and runs its base source behavior. Without this a regression
# in the analyzer-free variant would surface only at publish time.
- name: Verify CGO-free (companion) build variant
if: matrix.gate == 'coverage'
env:
CGO_ENABLED: "0"
run: |
go build -tags codefly_nosemantic -ldflags '-s -w -extldflags "-static"' -o /dev/null .
go test -tags codefly_nosemantic ./pkg/engine/ -run '^TestNewSourceExecutesLanguageNeutralOperation$' -count=1

- name: Test with race detection
if: matrix.gate == 'race'
run: go test -failfast -race ./... -v
Expand Down
8 changes: 8 additions & 0 deletions cmd/companion/build.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -279,6 +279,13 @@ func needsLinuxCLI(targets []*Companion) bool {
// the alpine images can run without glibc. Flag stripping (-s -w)
// drops the symbol table and DWARF info; ~25-30% size reduction with
// no runtime cost.
//
// The `codefly_nosemantic` tag drops the tree-sitter semantic analyzer, which
// cannot link without cgo. It is required (not merely implied by CGO_ENABLED=0):
// without it this CGO-free build would select the analyzer variant and fail to
// link. The companion CLI only builds and runs services in-container and never
// serves the semantic gateway, so the analyzer is not needed here. See
// pkg/engine/source_nosemantic.go.
func buildLinuxCLI(coreDir, arch string) error {
cliDir := filepath.Join(coreDir, "..", "cli")
if info, err := os.Stat(cliDir); err != nil || !info.IsDir() {
Expand All@@ -290,6 +297,7 @@ func buildLinuxCLI(coreDir, arch string) error {
}

cmd := exec.Command("go", "build",
"-tags", "codefly_nosemantic",
"-ldflags", `-s -w -extldflags "-static"`,
"-o", outBin,
".",
Expand Down
8 changes: 8 additions & 0 deletions cmd/self/build.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -245,12 +245,20 @@ func defaultCrossOutput(cliSrcDir, goos string) string {
// buildCLICross compiles a static CLI binary for goos/goarch. Static
// (CGO_ENABLED=0 + -extldflags "-static") so alpine images can run it
// without glibc; stripped (-s -w) for size. Replaces scripts/build/linux.sh.
//
// The `codefly_nosemantic` tag drops the tree-sitter semantic analyzer, which
// cannot link without cgo. It is required (not merely implied by CGO_ENABLED=0):
// without it this CGO-free build would select the analyzer variant and fail to
// link. This cross binary is an in-container artifact that never serves the
// semantic gateway, and it is never installed over the running CLI, so the
// analyzer is not needed here. See pkg/engine/source_nosemantic.go.
func buildCLICross(ctx context.Context, srcDir, output, goos, goarch string) error {
if err := os.MkdirAll(filepath.Dir(output), 0o755); err != nil {
return fmt.Errorf("create output dir: %w", err)
}
start := time.Now()
build := exec.CommandContext(ctx, "go", "build",
"-tags", "codefly_nosemantic",
"-ldflags", `-s -w -extldflags "-static"`,
"-o", output,
".",
Expand Down
9 changes: 0 additions & 9 deletions pkg/engine/source.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -9,7 +9,6 @@ import (

"github.com/codefly-dev/cli/pkg/sourceworkspace"
codecore "github.com/codefly-dev/core/code"
"github.com/codefly-dev/core/code/semantic"
codev0 "github.com/codefly-dev/core/generated/go/codefly/services/code/v0"
)

Expand All@@ -20,14 +19,6 @@ type Source struct {
server *codecore.DefaultCodeServer
}

// Core omits the tree-sitter analyzer by default so Go service agents stay
// CGO-free. The CLI is the workspace-wide source behavior behind the gateway —
// semantic index and symbol mutation are part of its contract — so it installs
// the analyzer explicitly. See core/code.WithSemanticAnalyzer.
func newSource(root string) *Source {
return &Source{server: codecore.NewDefaultCodeServer(root, codecore.WithSemanticAnalyzer(semantic.New()))}
}

// ExecuteCode executes a language-neutral Code request.
func (s *Source) ExecuteCode(ctx context.Context, request *codev0.CodeRequest) (*codev0.CodeResponse, error) {
if s == nil {
Expand Down
23 changes: 23 additions & 0 deletions pkg/engine/source_nosemantic.go
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
//go:build codefly_nosemantic

package engine

import codecore "github.com/codefly-dev/core/code"

// Without the tree-sitter semantic analyzer the source behavior runs on core's
// CGO-free default. This variant is selected only when a build explicitly sets
// the `codefly_nosemantic` tag; the tag is the intent, and cgo being disabled is
// merely how these builds also happen to be linked. Gating on an explicit tag
// (rather than on `!cgo`) means an accidental CGO_ENABLED=0 build still fails
// loudly instead of silently producing an analyzer-less CLI — see
// source_semantic.go.
//
// Two build paths opt in, both producing statically linked binaries for alpine
// (CGO_ENABLED=0, -extldflags "-static") that only build and run services
// in-container and never serve the semantic gateway, so the analyzer is not
// needed: `codefly companion build`/`publish` (cmd/companion/build.go) and
// `codefly self build --os/--arch` (cmd/self/build.go, buildCLICross). Neither
// installs over the user's running CLI, which keeps cgo and the analyzer.
func newSource(root string) *Source {
return &Source{server: codecore.NewDefaultCodeServer(root)}
}
24 changes: 24 additions & 0 deletions pkg/engine/source_semantic.go
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
//go:build !codefly_nosemantic

package engine

import (
codecore "github.com/codefly-dev/core/code"
"github.com/codefly-dev/core/code/semantic"
)

// Core omits the tree-sitter analyzer by default so Go service agents stay
// CGO-free. The CLI is the workspace-wide source behavior behind the gateway —
// semantic index and symbol mutation are part of its contract — so it installs
// the analyzer explicitly. See core/code.WithSemanticAnalyzer.
//
// This is the default variant: it is selected for every build that does NOT set
// the `codefly_nosemantic` tag. The analyzer's tree-sitter bindings require cgo,
// so a build that disables cgo without also setting the tag selects this file
// and fails to link ("build constraints exclude all Go files") — a loud signal
// that a normal CLI needs cgo, rather than a silent drop of the gateway. Only
// the in-container static builds that opt in with `codefly_nosemantic` get the
// analyzer-free variant — see source_nosemantic.go.
func newSource(root string) *Source {
return &Source{server: codecore.NewDefaultCodeServer(root, codecore.WithSemanticAnalyzer(semantic.New()))}
}
51 changes: 51 additions & 0 deletions pkg/engine/source_test.go
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
package engine

import (
"context"
"os"
"path/filepath"
"testing"

codev0 "github.com/codefly-dev/core/generated/go/codefly/services/code/v0"
)

// TestNewSourceExecutesLanguageNeutralOperation locks the newSource contract:
// whichever build variant is compiled (analyzer via source_semantic.go, or the
// CGO-free source_nosemantic.go selected by -tags codefly_nosemantic), the
// constructor must return a live Source whose base, language-neutral behavior
// works. Run under `-tags codefly_nosemantic` this is the only cli-side test
// that exercises the analyzer-free variant's runtime, closing the gap where the
// static companion build was verified to link but never to function.
func TestNewSourceExecutesLanguageNeutralOperation(t *testing.T) {
root := t.TempDir()
if err := os.WriteFile(filepath.Join(root, "marker.txt"), []byte("hello"), 0o644); err != nil {
t.Fatalf("seed source tree: %v", err)
}

source := newSource(root)
if source == nil {
t.Fatal("newSource returned nil")
}
t.Cleanup(func() { _ = source.Close() })

response, err := source.ExecuteCode(context.Background(), &codev0.CodeRequest{
Operation: &codev0.CodeRequest_ListFiles{ListFiles: &codev0.ListFilesRequest{}},
})
if err != nil {
t.Fatalf("ListFiles execute: %v", err)
}
if failure := response.GetFailure(); failure != nil {
t.Fatalf("ListFiles reported failure: %v", failure)
}

found := false
for _, file := range response.GetListFiles().GetFiles() {
if filepath.Base(file.GetPath()) == "marker.txt" {
found = true
break
}
}
if !found {
t.Fatalf("ListFiles did not return the seeded file; got %+v", response.GetListFiles().GetFiles())
}
}
Loading