1. New build system
All checks were successful
build-dsl-smoke / Build judge (push) Successful in 12s
build-dsl-smoke / debug / clang / linux (push) Successful in 6s
build-dsl-smoke / debug / gcc / linux (push) Successful in 8s
build-dsl-smoke / release / clang / linux (push) Successful in 8s
build-dsl-smoke / release / gcc / linux (push) Successful in 6s
build-dsl-smoke / sanitized / clang / linux (push) Successful in 8s
build-dsl-smoke / sanitized / gcc / linux (push) Successful in 7s
build-dsl-smoke / debug / clang / windows (push) Successful in 13s
build-dsl-smoke / debug-valgrind / gcc / linux (push) Successful in 14s
build-dsl-smoke / release / clang / windows (push) Successful in 16s
build-dsl-smoke / debug / msvc / windows (push) Successful in 18s
build-dsl-smoke / release / msvc / windows (push) Successful in 17s
build-dsl-smoke / SUMMARY (push) Successful in 4s
Release / Build & publish (push) Successful in 48s
All checks were successful
build-dsl-smoke / Build judge (push) Successful in 12s
build-dsl-smoke / debug / clang / linux (push) Successful in 6s
build-dsl-smoke / debug / gcc / linux (push) Successful in 8s
build-dsl-smoke / release / clang / linux (push) Successful in 8s
build-dsl-smoke / release / gcc / linux (push) Successful in 6s
build-dsl-smoke / sanitized / clang / linux (push) Successful in 8s
build-dsl-smoke / sanitized / gcc / linux (push) Successful in 7s
build-dsl-smoke / debug / clang / windows (push) Successful in 13s
build-dsl-smoke / debug-valgrind / gcc / linux (push) Successful in 14s
build-dsl-smoke / release / clang / windows (push) Successful in 16s
build-dsl-smoke / debug / msvc / windows (push) Successful in 18s
build-dsl-smoke / release / msvc / windows (push) Successful in 17s
build-dsl-smoke / SUMMARY (push) Successful in 4s
Release / Build & publish (push) Successful in 48s
Reviewed-on: #1
This commit was merged in pull request #1.
This commit is contained in:
25
dsl/ast.go
25
dsl/ast.go
@@ -7,13 +7,18 @@ type File struct {
|
||||
BuildLinux string
|
||||
BuildWindows string
|
||||
BuildDarwin string
|
||||
Timeout time.Duration
|
||||
MemoryLimit int64 // bytes; 0 means no limit
|
||||
Binary string // executable name produced by build (default: solution)
|
||||
Sources string // glob pattern for source files, expanded as $SOURCES in build
|
||||
|
||||
NormalizeCRLF bool // strip \r before matching stdout/stderr/outFiles
|
||||
TrimTrailingWS bool // trim trailing whitespace on each line before matching
|
||||
BuildDefaults *BuildConfig
|
||||
Builds []*BuildConfig
|
||||
Toolchains []*ToolchainSpec
|
||||
|
||||
Timeout time.Duration
|
||||
MemoryLimit int64
|
||||
Binary string
|
||||
Sources string
|
||||
|
||||
NormalizeCRLF bool
|
||||
TrimTrailingWS bool
|
||||
|
||||
Groups []*Group
|
||||
}
|
||||
@@ -25,7 +30,7 @@ type Group struct {
|
||||
MemoryLimit int64
|
||||
Env map[string]string
|
||||
Scoring ScoringMode
|
||||
Wrapper string // exec wrapper command (e.g., "valgrind --error-exitcode=1")
|
||||
Wrapper string
|
||||
|
||||
Tests []*Test
|
||||
Pattern *Pattern
|
||||
@@ -34,8 +39,8 @@ type Group struct {
|
||||
type ScoringMode int
|
||||
|
||||
const (
|
||||
ScoringPartial ScoringMode = iota // weight * passed/total (default)
|
||||
ScoringAllOrNone // weight or 0
|
||||
ScoringPartial ScoringMode = iota
|
||||
ScoringAllOrNone
|
||||
)
|
||||
|
||||
type Pattern struct {
|
||||
@@ -45,6 +50,8 @@ type Pattern struct {
|
||||
DirsGlob string
|
||||
InputFile string
|
||||
OutputFile string
|
||||
|
||||
Args []string
|
||||
}
|
||||
|
||||
func (p *Pattern) IsDirMode() bool {
|
||||
|
||||
158
dsl/build.go
Normal file
158
dsl/build.go
Normal file
@@ -0,0 +1,158 @@
|
||||
package dsl
|
||||
|
||||
type BuildProfile int
|
||||
|
||||
const (
|
||||
ProfileUnset BuildProfile = iota
|
||||
ProfileRelease
|
||||
ProfileDebug
|
||||
ProfileSanitized
|
||||
)
|
||||
|
||||
func (p BuildProfile) String() string {
|
||||
switch p {
|
||||
case ProfileRelease:
|
||||
return "release"
|
||||
case ProfileDebug:
|
||||
return "debug"
|
||||
case ProfileSanitized:
|
||||
return "sanitized"
|
||||
default:
|
||||
return "unset"
|
||||
}
|
||||
}
|
||||
|
||||
type WarningLevel int
|
||||
|
||||
const (
|
||||
WarningsUnset WarningLevel = iota
|
||||
WarningsDefault
|
||||
WarningsStrict
|
||||
WarningsPedantic
|
||||
)
|
||||
|
||||
func (w WarningLevel) String() string {
|
||||
switch w {
|
||||
case WarningsDefault:
|
||||
return "default"
|
||||
case WarningsStrict:
|
||||
return "strict"
|
||||
case WarningsPedantic:
|
||||
return "pedantic"
|
||||
default:
|
||||
return "unset"
|
||||
}
|
||||
}
|
||||
|
||||
type BuildConfig struct {
|
||||
Name string
|
||||
|
||||
Language string
|
||||
Standard string
|
||||
Sources []string
|
||||
Includes []string
|
||||
Output string
|
||||
|
||||
Profile BuildProfile
|
||||
Warnings WarningLevel
|
||||
Sanitize []string
|
||||
Wrapper string
|
||||
|
||||
Defines map[string]string
|
||||
Link []string
|
||||
Extra []string
|
||||
|
||||
Platforms []string
|
||||
Compilers []string
|
||||
|
||||
Linux *BuildConfig
|
||||
Windows *BuildConfig
|
||||
Darwin *BuildConfig
|
||||
}
|
||||
|
||||
func (dst *BuildConfig) MergeFrom(src *BuildConfig) {
|
||||
if src == nil {
|
||||
return
|
||||
}
|
||||
if src.Language != "" {
|
||||
dst.Language = src.Language
|
||||
}
|
||||
if src.Standard != "" {
|
||||
dst.Standard = src.Standard
|
||||
}
|
||||
if src.Output != "" {
|
||||
dst.Output = src.Output
|
||||
}
|
||||
if src.Profile != ProfileUnset {
|
||||
dst.Profile = src.Profile
|
||||
}
|
||||
if src.Warnings != WarningsUnset {
|
||||
dst.Warnings = src.Warnings
|
||||
}
|
||||
if src.Wrapper != "" {
|
||||
dst.Wrapper = src.Wrapper
|
||||
}
|
||||
|
||||
dst.Sources = append(dst.Sources, src.Sources...)
|
||||
dst.Includes = append(dst.Includes, src.Includes...)
|
||||
dst.Sanitize = append(dst.Sanitize, src.Sanitize...)
|
||||
dst.Link = append(dst.Link, src.Link...)
|
||||
dst.Extra = append(dst.Extra, src.Extra...)
|
||||
dst.Platforms = append(dst.Platforms, src.Platforms...)
|
||||
dst.Compilers = append(dst.Compilers, src.Compilers...)
|
||||
|
||||
if len(src.Defines) > 0 {
|
||||
if dst.Defines == nil {
|
||||
dst.Defines = map[string]string{}
|
||||
}
|
||||
for k, v := range src.Defines {
|
||||
dst.Defines[k] = v
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (b *BuildConfig) Resolve(defaults *BuildConfig, os string) BuildConfig {
|
||||
var out BuildConfig
|
||||
out.MergeFrom(defaults)
|
||||
out.MergeFrom(b)
|
||||
out.Name = b.Name
|
||||
|
||||
var osOverride *BuildConfig
|
||||
switch os {
|
||||
case "linux":
|
||||
osOverride = b.Linux
|
||||
case "windows":
|
||||
osOverride = b.Windows
|
||||
case "darwin":
|
||||
osOverride = b.Darwin
|
||||
}
|
||||
out.MergeFrom(osOverride)
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
func (b *BuildConfig) AppliesTo(os, compiler string) bool {
|
||||
if len(b.Platforms) > 0 && !contains(b.Platforms, os) {
|
||||
return false
|
||||
}
|
||||
if len(b.Compilers) > 0 && !contains(b.Compilers, compiler) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func contains(xs []string, x string) bool {
|
||||
for _, v := range xs {
|
||||
if v == x {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type ToolchainSpec struct {
|
||||
Name string
|
||||
Platforms []string
|
||||
Binary string
|
||||
Class string
|
||||
}
|
||||
355
dsl/build_parser.go
Normal file
355
dsl/build_parser.go
Normal file
@@ -0,0 +1,355 @@
|
||||
package dsl
|
||||
|
||||
import "fmt"
|
||||
|
||||
func (p *Parser) parseBuildBlock(name string) (*BuildConfig, error) {
|
||||
return p.parseBuildBlockInner(name, false)
|
||||
}
|
||||
|
||||
func (p *Parser) parseBuildBlockInner(name string, inOSOverride bool) (*BuildConfig, error) {
|
||||
if _, err := p.expect(TOKEN_LBRACE); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
bc := &BuildConfig{Name: name}
|
||||
|
||||
for !p.isRBrace() {
|
||||
t := p.peek()
|
||||
if t.Type != TOKEN_IDENT {
|
||||
return nil, fmt.Errorf("%d:%d: unexpected token %q in build block", t.Line, t.Col, t.Value)
|
||||
}
|
||||
|
||||
switch t.Value {
|
||||
case "language":
|
||||
p.advance()
|
||||
s, err := p.parseAssignString()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
bc.Language = s
|
||||
|
||||
case "standard":
|
||||
p.advance()
|
||||
s, err := p.parseAssignString()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
bc.Standard = s
|
||||
|
||||
case "output":
|
||||
p.advance()
|
||||
s, err := p.parseAssignString()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
bc.Output = s
|
||||
|
||||
case "wrapper":
|
||||
p.advance()
|
||||
s, err := p.parseAssignString()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
bc.Wrapper = s
|
||||
|
||||
case "sources":
|
||||
p.advance()
|
||||
xs, err := p.parseAssignStringList()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
bc.Sources = xs
|
||||
|
||||
case "includes":
|
||||
p.advance()
|
||||
xs, err := p.parseAssignStringList()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
bc.Includes = xs
|
||||
|
||||
case "sanitize":
|
||||
p.advance()
|
||||
xs, err := p.parseAssignStringList()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
bc.Sanitize = xs
|
||||
|
||||
case "link":
|
||||
p.advance()
|
||||
xs, err := p.parseAssignStringList()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
bc.Link = xs
|
||||
|
||||
case "extra":
|
||||
p.advance()
|
||||
xs, err := p.parseAssignStringList()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
bc.Extra = xs
|
||||
|
||||
case "platforms":
|
||||
p.advance()
|
||||
xs, err := p.parseAssignStringList()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := validatePlatformList(xs, t.Line, t.Col); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
bc.Platforms = xs
|
||||
|
||||
case "compilers":
|
||||
p.advance()
|
||||
xs, err := p.parseAssignStringList()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
bc.Compilers = xs
|
||||
|
||||
case "profile":
|
||||
p.advance()
|
||||
if _, err := p.expect(TOKEN_ASSIGN); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
id, err := p.expect(TOKEN_IDENT)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
prof, err := parseProfileIdent(id.Value, id.Line, id.Col)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
bc.Profile = prof
|
||||
|
||||
case "warnings":
|
||||
p.advance()
|
||||
if _, err := p.expect(TOKEN_ASSIGN); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
id, err := p.expect(TOKEN_IDENT)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
w, err := parseWarningsIdent(id.Value, id.Line, id.Col)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
bc.Warnings = w
|
||||
|
||||
case "define":
|
||||
p.advance()
|
||||
if _, err := p.expect(TOKEN_LPAREN); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
key, err := p.expect(TOKEN_STRING)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, err := p.expect(TOKEN_RPAREN); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, err := p.expect(TOKEN_ASSIGN); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
val, err := p.expect(TOKEN_STRING)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if bc.Defines == nil {
|
||||
bc.Defines = map[string]string{}
|
||||
}
|
||||
bc.Defines[key.Value] = val.Value
|
||||
|
||||
case "linux", "windows", "darwin":
|
||||
if inOSOverride {
|
||||
return nil, fmt.Errorf("%d:%d: OS override %q cannot be nested inside another OS override", t.Line, t.Col, t.Value)
|
||||
}
|
||||
osName := t.Value
|
||||
p.advance()
|
||||
sub, err := p.parseBuildBlockInner("", true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
switch osName {
|
||||
case "linux":
|
||||
if bc.Linux != nil {
|
||||
return nil, fmt.Errorf("%d:%d: duplicate linux override", t.Line, t.Col)
|
||||
}
|
||||
bc.Linux = sub
|
||||
case "windows":
|
||||
if bc.Windows != nil {
|
||||
return nil, fmt.Errorf("%d:%d: duplicate windows override", t.Line, t.Col)
|
||||
}
|
||||
bc.Windows = sub
|
||||
case "darwin":
|
||||
if bc.Darwin != nil {
|
||||
return nil, fmt.Errorf("%d:%d: duplicate darwin override", t.Line, t.Col)
|
||||
}
|
||||
bc.Darwin = sub
|
||||
}
|
||||
|
||||
default:
|
||||
return nil, fmt.Errorf("%d:%d: unknown field %q in build block", t.Line, t.Col, t.Value)
|
||||
}
|
||||
}
|
||||
|
||||
if _, err := p.expect(TOKEN_RBRACE); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return bc, nil
|
||||
}
|
||||
|
||||
func (p *Parser) parseAssignString() (string, error) {
|
||||
if _, err := p.expect(TOKEN_ASSIGN); err != nil {
|
||||
return "", err
|
||||
}
|
||||
s, err := p.expect(TOKEN_STRING)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return s.Value, nil
|
||||
}
|
||||
|
||||
func (p *Parser) parseAssignStringList() ([]string, error) {
|
||||
if _, err := p.expect(TOKEN_ASSIGN); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return p.parseStringList()
|
||||
}
|
||||
|
||||
func parseProfileIdent(v string, line, col int) (BuildProfile, error) {
|
||||
switch v {
|
||||
case "release":
|
||||
return ProfileRelease, nil
|
||||
case "debug":
|
||||
return ProfileDebug, nil
|
||||
case "sanitized":
|
||||
return ProfileSanitized, nil
|
||||
default:
|
||||
return ProfileUnset, fmt.Errorf("%d:%d: unknown profile %q (expected release/debug/sanitized)", line, col, v)
|
||||
}
|
||||
}
|
||||
|
||||
func parseWarningsIdent(v string, line, col int) (WarningLevel, error) {
|
||||
switch v {
|
||||
case "default":
|
||||
return WarningsDefault, nil
|
||||
case "strict":
|
||||
return WarningsStrict, nil
|
||||
case "pedantic":
|
||||
return WarningsPedantic, nil
|
||||
default:
|
||||
return WarningsUnset, fmt.Errorf("%d:%d: unknown warnings level %q (expected default/strict/pedantic)", line, col, v)
|
||||
}
|
||||
}
|
||||
|
||||
func validatePlatformList(xs []string, line, col int) error {
|
||||
for _, x := range xs {
|
||||
switch x {
|
||||
case "linux", "windows", "darwin":
|
||||
default:
|
||||
return fmt.Errorf("%d:%d: unknown platform %q (expected linux/windows/darwin)", line, col, x)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Parser) parseToolchainsBlock() ([]*ToolchainSpec, error) {
|
||||
if _, err := p.expect(TOKEN_LBRACE); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var specs []*ToolchainSpec
|
||||
seen := map[string]bool{}
|
||||
|
||||
for !p.isRBrace() {
|
||||
nameTok := p.peek()
|
||||
if nameTok.Type != TOKEN_IDENT && nameTok.Type != TOKEN_STRING {
|
||||
return nil, fmt.Errorf("%d:%d: expected toolchain name, got %q", nameTok.Line, nameTok.Col, nameTok.Value)
|
||||
}
|
||||
p.advance()
|
||||
name := nameTok.Value
|
||||
if seen[name] {
|
||||
return nil, fmt.Errorf("%d:%d: duplicate toolchain %q", nameTok.Line, nameTok.Col, name)
|
||||
}
|
||||
seen[name] = true
|
||||
|
||||
spec, err := p.parseToolchainEntry(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
specs = append(specs, spec)
|
||||
}
|
||||
|
||||
if _, err := p.expect(TOKEN_RBRACE); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return specs, nil
|
||||
}
|
||||
|
||||
func (p *Parser) parseToolchainEntry(name string) (*ToolchainSpec, error) {
|
||||
if _, err := p.expect(TOKEN_LBRACE); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
spec := &ToolchainSpec{Name: name}
|
||||
for !p.isRBrace() {
|
||||
t := p.peek()
|
||||
if t.Type != TOKEN_IDENT {
|
||||
return nil, fmt.Errorf("%d:%d: unexpected token %q in toolchain block", t.Line, t.Col, t.Value)
|
||||
}
|
||||
switch t.Value {
|
||||
case "platforms":
|
||||
p.advance()
|
||||
xs, err := p.parseAssignStringList()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := validatePlatformList(xs, t.Line, t.Col); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
spec.Platforms = xs
|
||||
|
||||
case "binary":
|
||||
p.advance()
|
||||
s, err := p.parseAssignString()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
spec.Binary = s
|
||||
|
||||
case "class":
|
||||
p.advance()
|
||||
if _, err := p.expect(TOKEN_ASSIGN); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
id, err := p.expect(TOKEN_IDENT)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
switch id.Value {
|
||||
case "gnu", "msvc":
|
||||
default:
|
||||
return nil, fmt.Errorf("%d:%d: unknown compiler class %q (expected gnu/msvc)", id.Line, id.Col, id.Value)
|
||||
}
|
||||
spec.Class = id.Value
|
||||
|
||||
default:
|
||||
return nil, fmt.Errorf("%d:%d: unknown field %q in toolchain block", t.Line, t.Col, t.Value)
|
||||
}
|
||||
}
|
||||
|
||||
if _, err := p.expect(TOKEN_RBRACE); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(spec.Platforms) == 0 {
|
||||
return nil, fmt.Errorf("toolchain %q: platforms is required", name)
|
||||
}
|
||||
return spec, nil
|
||||
}
|
||||
455
dsl/build_test.go
Normal file
455
dsl/build_test.go
Normal file
@@ -0,0 +1,455 @@
|
||||
package dsl
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestParseBuildBlockMinimal(t *testing.T) {
|
||||
src := `
|
||||
build_defaults {
|
||||
language = "c"
|
||||
standard = "c11"
|
||||
sources = "*.c"
|
||||
output = "solution"
|
||||
warnings = strict
|
||||
}
|
||||
|
||||
build "release" {
|
||||
profile = release
|
||||
}
|
||||
|
||||
build "debug" {
|
||||
profile = debug
|
||||
}
|
||||
|
||||
group("g1") {
|
||||
weight = 1.0
|
||||
test("t1") {
|
||||
stdout = "ok\n"
|
||||
}
|
||||
}
|
||||
`
|
||||
f, _, err := Parse(src)
|
||||
if err != nil {
|
||||
t.Fatalf("parse: %v", err)
|
||||
}
|
||||
|
||||
if f.BuildDefaults == nil {
|
||||
t.Fatal("BuildDefaults not populated")
|
||||
}
|
||||
if f.BuildDefaults.Language != "c" {
|
||||
t.Errorf("language = %q", f.BuildDefaults.Language)
|
||||
}
|
||||
if f.BuildDefaults.Standard != "c11" {
|
||||
t.Errorf("standard = %q", f.BuildDefaults.Standard)
|
||||
}
|
||||
if len(f.BuildDefaults.Sources) != 1 || f.BuildDefaults.Sources[0] != "*.c" {
|
||||
t.Errorf("sources = %v", f.BuildDefaults.Sources)
|
||||
}
|
||||
if f.BuildDefaults.Output != "solution" {
|
||||
t.Errorf("output = %q", f.BuildDefaults.Output)
|
||||
}
|
||||
if f.BuildDefaults.Warnings != WarningsStrict {
|
||||
t.Errorf("warnings = %v", f.BuildDefaults.Warnings)
|
||||
}
|
||||
|
||||
if len(f.Builds) != 2 {
|
||||
t.Fatalf("expected 2 builds, got %d", len(f.Builds))
|
||||
}
|
||||
if f.Builds[0].Name != "release" || f.Builds[0].Profile != ProfileRelease {
|
||||
t.Errorf("builds[0] = %+v", f.Builds[0])
|
||||
}
|
||||
if f.Builds[1].Name != "debug" || f.Builds[1].Profile != ProfileDebug {
|
||||
t.Errorf("builds[1] = %+v", f.Builds[1])
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseBuildLegacyStillWorks(t *testing.T) {
|
||||
src := `
|
||||
build "cc -O2 solution.c -o solution"
|
||||
timeout 5s
|
||||
|
||||
group("g1") {
|
||||
weight = 1.0
|
||||
test("t1") { stdout = "ok\n" }
|
||||
}
|
||||
`
|
||||
f, _, err := Parse(src)
|
||||
if err != nil {
|
||||
t.Fatalf("parse: %v", err)
|
||||
}
|
||||
if f.Build != "cc -O2 solution.c -o solution" {
|
||||
t.Errorf("legacy build = %q", f.Build)
|
||||
}
|
||||
if f.BuildDefaults != nil || len(f.Builds) != 0 {
|
||||
t.Errorf("structured fields should be empty for legacy form")
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseBuildOSOverride(t *testing.T) {
|
||||
src := `
|
||||
build_defaults {
|
||||
language = "c"
|
||||
standard = "c11"
|
||||
sources = "*.c"
|
||||
output = "solution"
|
||||
}
|
||||
|
||||
build "release" {
|
||||
profile = release
|
||||
linux { extra = "-fPIC" }
|
||||
windows { extra = "/bigobj" }
|
||||
}
|
||||
|
||||
group("g1") {
|
||||
weight = 1.0
|
||||
test("t1") { stdout = "ok\n" }
|
||||
}
|
||||
`
|
||||
f, _, err := Parse(src)
|
||||
if err != nil {
|
||||
t.Fatalf("parse: %v", err)
|
||||
}
|
||||
b := f.Builds[0]
|
||||
if b.Linux == nil || len(b.Linux.Extra) != 1 || b.Linux.Extra[0] != "-fPIC" {
|
||||
t.Errorf("linux override = %+v", b.Linux)
|
||||
}
|
||||
if b.Windows == nil || len(b.Windows.Extra) != 1 || b.Windows.Extra[0] != "/bigobj" {
|
||||
t.Errorf("windows override = %+v", b.Windows)
|
||||
}
|
||||
if b.Darwin != nil {
|
||||
t.Errorf("darwin override should be nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseBuildPlatformsFilter(t *testing.T) {
|
||||
src := `
|
||||
build "sanitized" {
|
||||
profile = sanitized
|
||||
sanitize = "address" "undefined"
|
||||
platforms = "linux"
|
||||
compilers = "gcc" "clang"
|
||||
}
|
||||
|
||||
group("g1") {
|
||||
weight = 1.0
|
||||
test("t1") { stdout = "ok\n" }
|
||||
}
|
||||
`
|
||||
f, _, err := Parse(src)
|
||||
if err != nil {
|
||||
t.Fatalf("parse: %v", err)
|
||||
}
|
||||
b := f.Builds[0]
|
||||
if b.Profile != ProfileSanitized {
|
||||
t.Errorf("profile = %v", b.Profile)
|
||||
}
|
||||
if len(b.Sanitize) != 2 || b.Sanitize[0] != "address" || b.Sanitize[1] != "undefined" {
|
||||
t.Errorf("sanitize = %v", b.Sanitize)
|
||||
}
|
||||
if len(b.Platforms) != 1 || b.Platforms[0] != "linux" {
|
||||
t.Errorf("platforms = %v", b.Platforms)
|
||||
}
|
||||
if len(b.Compilers) != 2 {
|
||||
t.Errorf("compilers = %v", b.Compilers)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseBuildDefineField(t *testing.T) {
|
||||
src := `
|
||||
build "rel" {
|
||||
profile = release
|
||||
define("NDEBUG") = "1"
|
||||
define("VERSION") = "42"
|
||||
}
|
||||
|
||||
group("g1") {
|
||||
weight = 1.0
|
||||
test("t1") { stdout = "ok\n" }
|
||||
}
|
||||
`
|
||||
f, _, err := Parse(src)
|
||||
if err != nil {
|
||||
t.Fatalf("parse: %v", err)
|
||||
}
|
||||
d := f.Builds[0].Defines
|
||||
if d["NDEBUG"] != "1" || d["VERSION"] != "42" {
|
||||
t.Errorf("defines = %v", d)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseBuildErrors(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
src string
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "unknown profile",
|
||||
src: `
|
||||
build "x" { profile = ultra }
|
||||
group("g") { weight=1.0 test("t"){ stdout="ok\n" } }
|
||||
`,
|
||||
want: "unknown profile",
|
||||
},
|
||||
{
|
||||
name: "unknown warnings",
|
||||
src: `
|
||||
build "x" { warnings = insane }
|
||||
group("g") { weight=1.0 test("t"){ stdout="ok\n" } }
|
||||
`,
|
||||
want: "unknown warnings",
|
||||
},
|
||||
{
|
||||
name: "unknown platform",
|
||||
src: `
|
||||
build "x" { platforms = "bsd" }
|
||||
group("g") { weight=1.0 test("t"){ stdout="ok\n" } }
|
||||
`,
|
||||
want: "unknown platform",
|
||||
},
|
||||
{
|
||||
name: "nested OS override",
|
||||
src: `
|
||||
build "x" {
|
||||
linux {
|
||||
windows { extra = "nope" }
|
||||
}
|
||||
}
|
||||
group("g") { weight=1.0 test("t"){ stdout="ok\n" } }
|
||||
`,
|
||||
want: "cannot be nested",
|
||||
},
|
||||
{
|
||||
name: "duplicate OS override",
|
||||
src: `
|
||||
build "x" {
|
||||
linux { extra = "-a" }
|
||||
linux { extra = "-b" }
|
||||
}
|
||||
group("g") { weight=1.0 test("t"){ stdout="ok\n" } }
|
||||
`,
|
||||
want: "duplicate linux override",
|
||||
},
|
||||
{
|
||||
name: "unknown field",
|
||||
src: `
|
||||
build "x" { magic = "yes" }
|
||||
group("g") { weight=1.0 test("t"){ stdout="ok\n" } }
|
||||
`,
|
||||
want: "unknown field",
|
||||
},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
_, _, err := Parse(c.src)
|
||||
if err == nil {
|
||||
t.Fatal("expected error, got nil")
|
||||
}
|
||||
if !strings.Contains(err.Error(), c.want) {
|
||||
t.Errorf("error %q does not contain %q", err.Error(), c.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseBuildMixedLegacyAndStructuredRejected(t *testing.T) {
|
||||
src := `
|
||||
build "cc -O2 solution.c -o solution"
|
||||
build "release" { profile = release }
|
||||
group("g") { weight = 1.0 test("t") { stdout = "ok\n" } }
|
||||
`
|
||||
_, _, err := Parse(src)
|
||||
if err == nil {
|
||||
t.Fatal("expected error mixing legacy and structured builds")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "cannot mix") {
|
||||
t.Errorf("error %q does not mention mixing", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseBuildDuplicateNameRejected(t *testing.T) {
|
||||
src := `
|
||||
build "release" { profile = release }
|
||||
build "release" { profile = debug }
|
||||
group("g") { weight = 1.0 test("t") { stdout = "ok\n" } }
|
||||
`
|
||||
_, _, err := Parse(src)
|
||||
if err == nil {
|
||||
t.Fatal("expected error on duplicate build name")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "duplicate") {
|
||||
t.Errorf("error %q does not mention duplicate", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildConfigResolveMerge(t *testing.T) {
|
||||
defaults := &BuildConfig{
|
||||
Language: "c",
|
||||
Standard: "c11",
|
||||
Sources: []string{"*.c"},
|
||||
Output: "solution",
|
||||
Warnings: WarningsStrict,
|
||||
}
|
||||
b := &BuildConfig{
|
||||
Name: "release",
|
||||
Profile: ProfileRelease,
|
||||
Extra: []string{"-DFOO"},
|
||||
Linux: &BuildConfig{
|
||||
Extra: []string{"-fPIC"},
|
||||
},
|
||||
Windows: &BuildConfig{
|
||||
Extra: []string{"/bigobj"},
|
||||
Output: "solution",
|
||||
},
|
||||
}
|
||||
|
||||
linux := b.Resolve(defaults, "linux")
|
||||
if linux.Language != "c" || linux.Standard != "c11" {
|
||||
t.Errorf("linux defaults not merged: %+v", linux)
|
||||
}
|
||||
if linux.Profile != ProfileRelease {
|
||||
t.Errorf("linux profile = %v", linux.Profile)
|
||||
}
|
||||
if linux.Warnings != WarningsStrict {
|
||||
t.Errorf("linux warnings = %v", linux.Warnings)
|
||||
}
|
||||
if len(linux.Sources) != 1 || linux.Sources[0] != "*.c" {
|
||||
t.Errorf("linux sources = %v", linux.Sources)
|
||||
}
|
||||
if len(linux.Extra) != 2 || linux.Extra[0] != "-DFOO" || linux.Extra[1] != "-fPIC" {
|
||||
t.Errorf("linux extra = %v", linux.Extra)
|
||||
}
|
||||
|
||||
windows := b.Resolve(defaults, "windows")
|
||||
if len(windows.Extra) != 2 || windows.Extra[0] != "-DFOO" || windows.Extra[1] != "/bigobj" {
|
||||
t.Errorf("windows extra = %v", windows.Extra)
|
||||
}
|
||||
|
||||
darwin := b.Resolve(defaults, "darwin")
|
||||
if len(darwin.Extra) != 1 || darwin.Extra[0] != "-DFOO" {
|
||||
t.Errorf("darwin extra (no override) = %v", darwin.Extra)
|
||||
}
|
||||
|
||||
if len(b.Extra) != 1 {
|
||||
t.Errorf("receiver mutated: %v", b.Extra)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseToolchainsBlock(t *testing.T) {
|
||||
src := `
|
||||
toolchains {
|
||||
gcc { platforms = "linux" }
|
||||
clang {
|
||||
platforms = "linux" "windows"
|
||||
binary = "clang-17"
|
||||
class = gnu
|
||||
}
|
||||
msvc { platforms = "windows" }
|
||||
nvcc {
|
||||
platforms = "linux"
|
||||
class = gnu
|
||||
}
|
||||
}
|
||||
|
||||
build "release" { profile = release }
|
||||
|
||||
group("g") { weight = 1.0 test("t") { stdout = "ok\n" } }
|
||||
`
|
||||
f, _, err := Parse(src)
|
||||
if err != nil {
|
||||
t.Fatalf("parse: %v", err)
|
||||
}
|
||||
if len(f.Toolchains) != 4 {
|
||||
t.Fatalf("expected 4 toolchains, got %d", len(f.Toolchains))
|
||||
}
|
||||
gcc := f.Toolchains[0]
|
||||
if gcc.Name != "gcc" || len(gcc.Platforms) != 1 || gcc.Platforms[0] != "linux" {
|
||||
t.Errorf("gcc = %+v", gcc)
|
||||
}
|
||||
clang := f.Toolchains[1]
|
||||
if clang.Name != "clang" || len(clang.Platforms) != 2 || clang.Binary != "clang-17" || clang.Class != "gnu" {
|
||||
t.Errorf("clang = %+v", clang)
|
||||
}
|
||||
if clang.Platforms[0] != "linux" || clang.Platforms[1] != "windows" {
|
||||
t.Errorf("clang platforms = %v", clang.Platforms)
|
||||
}
|
||||
nvcc := f.Toolchains[3]
|
||||
if nvcc.Name != "nvcc" || len(nvcc.Platforms) != 1 || nvcc.Platforms[0] != "linux" || nvcc.Class != "gnu" {
|
||||
t.Errorf("nvcc = %+v", nvcc)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseToolchainsErrors(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
src string
|
||||
want string
|
||||
}{
|
||||
{
|
||||
"missing platforms",
|
||||
`toolchains { gcc { } }
|
||||
group("g") { weight=1.0 test("t") { stdout="ok\n" } }`,
|
||||
"platforms is required",
|
||||
},
|
||||
{
|
||||
"unknown platform",
|
||||
`toolchains { gcc { platforms = "bsd" } }
|
||||
group("g") { weight=1.0 test("t") { stdout="ok\n" } }`,
|
||||
"unknown platform",
|
||||
},
|
||||
{
|
||||
"unknown class",
|
||||
`toolchains { gcc { platforms = "linux" class = llvm } }
|
||||
group("g") { weight=1.0 test("t") { stdout="ok\n" } }`,
|
||||
"unknown compiler class",
|
||||
},
|
||||
{
|
||||
"duplicate toolchain",
|
||||
`toolchains {
|
||||
gcc { platforms = "linux" }
|
||||
gcc { platforms = "linux" }
|
||||
}
|
||||
group("g") { weight=1.0 test("t") { stdout="ok\n" } }`,
|
||||
"duplicate toolchain",
|
||||
},
|
||||
}
|
||||
for _, c := range cases {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
_, _, err := Parse(c.src)
|
||||
if err == nil {
|
||||
t.Fatal("expected error, got nil")
|
||||
}
|
||||
if !strings.Contains(err.Error(), c.want) {
|
||||
t.Errorf("error %q does not contain %q", err.Error(), c.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildConfigAppliesTo(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
b BuildConfig
|
||||
os, cc string
|
||||
expected bool
|
||||
}{
|
||||
{"no filters", BuildConfig{}, "linux", "gcc", true},
|
||||
{"os match", BuildConfig{Platforms: []string{"linux"}}, "linux", "gcc", true},
|
||||
{"os mismatch", BuildConfig{Platforms: []string{"linux"}}, "windows", "msvc", false},
|
||||
{"cc match", BuildConfig{Compilers: []string{"gcc", "clang"}}, "linux", "clang", true},
|
||||
{"cc mismatch", BuildConfig{Compilers: []string{"gcc"}}, "linux", "msvc", false},
|
||||
{"both match", BuildConfig{Platforms: []string{"linux"}, Compilers: []string{"gcc"}}, "linux", "gcc", true},
|
||||
{"os ok cc bad", BuildConfig{Platforms: []string{"linux"}, Compilers: []string{"gcc"}}, "linux", "clang", false},
|
||||
}
|
||||
for _, c := range cases {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
got := c.b.AppliesTo(c.os, c.cc)
|
||||
if got != c.expected {
|
||||
t.Errorf("AppliesTo(%q, %q) = %v, want %v", c.os, c.cc, got, c.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
210
dsl/include_test.go
Normal file
210
dsl/include_test.go
Normal file
@@ -0,0 +1,210 @@
|
||||
package dsl
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func writeTempJdg(t *testing.T, dir, name, content string) string {
|
||||
t.Helper()
|
||||
path := filepath.Join(dir, name)
|
||||
if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(path, []byte(content), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
func TestIncludeBasicMerge(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
|
||||
writeTempJdg(t, dir, "common.jdg", `
|
||||
toolchains {
|
||||
gcc { platforms = "linux" }
|
||||
}
|
||||
|
||||
build_defaults {
|
||||
language = "c"
|
||||
standard = "c11"
|
||||
sources = "*.c"
|
||||
output = "solution"
|
||||
warnings = strict
|
||||
}
|
||||
`)
|
||||
|
||||
mainPath := writeTempJdg(t, dir, "main.jdg", `
|
||||
include "common.jdg"
|
||||
|
||||
build "release" { profile = release }
|
||||
|
||||
group("g") {
|
||||
weight = 1.0
|
||||
test("t") { stdout = "ok\n" }
|
||||
}
|
||||
`)
|
||||
|
||||
f, _, err := ParseFile(mainPath)
|
||||
if err != nil {
|
||||
t.Fatalf("parse: %v", err)
|
||||
}
|
||||
|
||||
if len(f.Toolchains) != 1 || f.Toolchains[0].Name != "gcc" {
|
||||
t.Errorf("toolchains not merged: %+v", f.Toolchains)
|
||||
}
|
||||
if f.BuildDefaults == nil || f.BuildDefaults.Language != "c" {
|
||||
t.Errorf("build_defaults not merged: %+v", f.BuildDefaults)
|
||||
}
|
||||
if len(f.Builds) != 1 || f.Builds[0].Name != "release" {
|
||||
t.Errorf("local build lost: %+v", f.Builds)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIncludeLocalOverridesIncluded(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
|
||||
writeTempJdg(t, dir, "common.jdg", `
|
||||
build_defaults {
|
||||
language = "c"
|
||||
standard = "c11"
|
||||
warnings = strict
|
||||
}
|
||||
`)
|
||||
|
||||
mainPath := writeTempJdg(t, dir, "main.jdg", `
|
||||
include "common.jdg"
|
||||
|
||||
build_defaults {
|
||||
standard = "c17"
|
||||
}
|
||||
|
||||
group("g") { weight = 1.0 test("t") { stdout = "ok\n" } }
|
||||
`)
|
||||
|
||||
f, _, err := ParseFile(mainPath)
|
||||
if err != nil {
|
||||
t.Fatalf("parse: %v", err)
|
||||
}
|
||||
if f.BuildDefaults.Standard != "c17" {
|
||||
t.Errorf("local should override included standard: got %q", f.BuildDefaults.Standard)
|
||||
}
|
||||
if f.BuildDefaults.Language != "c" {
|
||||
t.Errorf("language should survive from include: got %q", f.BuildDefaults.Language)
|
||||
}
|
||||
if f.BuildDefaults.Warnings != WarningsStrict {
|
||||
t.Errorf("warnings should survive from include: got %v", f.BuildDefaults.Warnings)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIncludeRelativePathResolution(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
|
||||
writeTempJdg(t, dir, "shared/tools.jdg", `
|
||||
toolchains {
|
||||
gcc { platforms = "linux" }
|
||||
}
|
||||
`)
|
||||
|
||||
mainPath := writeTempJdg(t, dir, "suite/main.jdg", `
|
||||
include "../shared/tools.jdg"
|
||||
|
||||
build "release" {
|
||||
language = "c"
|
||||
sources = "solution.c"
|
||||
output = "solution"
|
||||
profile = release
|
||||
}
|
||||
|
||||
group("g") { weight = 1.0 test("t") { stdout = "ok\n" } }
|
||||
`)
|
||||
|
||||
f, _, err := ParseFile(mainPath)
|
||||
if err != nil {
|
||||
t.Fatalf("parse: %v", err)
|
||||
}
|
||||
if len(f.Toolchains) != 1 {
|
||||
t.Errorf("relative include failed to resolve: %+v", f.Toolchains)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIncludeDuplicateToolchainErrors(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
|
||||
writeTempJdg(t, dir, "common.jdg", `
|
||||
toolchains {
|
||||
gcc { platforms = "linux" }
|
||||
}
|
||||
`)
|
||||
|
||||
mainPath := writeTempJdg(t, dir, "main.jdg", `
|
||||
include "common.jdg"
|
||||
|
||||
toolchains {
|
||||
gcc { platforms = "linux" }
|
||||
}
|
||||
|
||||
group("g") { weight = 1.0 test("t") { stdout = "ok\n" } }
|
||||
`)
|
||||
|
||||
_, _, err := ParseFile(mainPath)
|
||||
if err == nil {
|
||||
t.Fatal("expected duplicate toolchain error")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "duplicate toolchain") {
|
||||
t.Errorf("error %q does not mention duplicate toolchain", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func TestIncludeCircularDetection(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
|
||||
aPath := writeTempJdg(t, dir, "a.jdg", `
|
||||
include "b.jdg"
|
||||
group("ga") { weight = 1.0 test("t") { stdout = "ok\n" } }
|
||||
`)
|
||||
writeTempJdg(t, dir, "b.jdg", `
|
||||
include "a.jdg"
|
||||
`)
|
||||
|
||||
_, _, err := ParseFile(aPath)
|
||||
if err == nil {
|
||||
t.Fatal("expected circular include error")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "circular include") {
|
||||
t.Errorf("error %q does not mention circular include", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func TestIncludeMissingFileErrors(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
|
||||
mainPath := writeTempJdg(t, dir, "main.jdg", `
|
||||
include "nonexistent.jdg"
|
||||
group("g") { weight = 1.0 test("t") { stdout = "ok\n" } }
|
||||
`)
|
||||
|
||||
_, _, err := ParseFile(mainPath)
|
||||
if err == nil {
|
||||
t.Fatal("expected missing include error")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "nonexistent.jdg") {
|
||||
t.Errorf("error %q does not reference the missing path", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseRejectsIncludeWithoutFileContext(t *testing.T) {
|
||||
src := `
|
||||
include "common.jdg"
|
||||
group("g") { weight = 1.0 test("t") { stdout = "ok\n" } }
|
||||
`
|
||||
_, _, err := Parse(src)
|
||||
if err == nil {
|
||||
t.Fatal("expected error: include without file context")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "file context") {
|
||||
t.Errorf("error %q does not mention file context", err.Error())
|
||||
}
|
||||
}
|
||||
81
dsl/merge.go
Normal file
81
dsl/merge.go
Normal file
@@ -0,0 +1,81 @@
|
||||
package dsl
|
||||
|
||||
import "fmt"
|
||||
|
||||
func mergeFiles(dst, src *File) error {
|
||||
if src.Build != "" {
|
||||
dst.Build = src.Build
|
||||
}
|
||||
if src.BuildLinux != "" {
|
||||
dst.BuildLinux = src.BuildLinux
|
||||
}
|
||||
if src.BuildWindows != "" {
|
||||
dst.BuildWindows = src.BuildWindows
|
||||
}
|
||||
if src.BuildDarwin != "" {
|
||||
dst.BuildDarwin = src.BuildDarwin
|
||||
}
|
||||
|
||||
if src.BuildDefaults != nil {
|
||||
if dst.BuildDefaults == nil {
|
||||
dst.BuildDefaults = &BuildConfig{}
|
||||
}
|
||||
dst.BuildDefaults.MergeFrom(src.BuildDefaults)
|
||||
}
|
||||
|
||||
seenTC := map[string]bool{}
|
||||
for _, t := range dst.Toolchains {
|
||||
seenTC[t.Name] = true
|
||||
}
|
||||
for _, t := range src.Toolchains {
|
||||
if seenTC[t.Name] {
|
||||
return fmt.Errorf("duplicate toolchain %q", t.Name)
|
||||
}
|
||||
seenTC[t.Name] = true
|
||||
dst.Toolchains = append(dst.Toolchains, t)
|
||||
}
|
||||
|
||||
seenB := map[string]bool{}
|
||||
for _, b := range dst.Builds {
|
||||
seenB[b.Name] = true
|
||||
}
|
||||
for _, b := range src.Builds {
|
||||
if seenB[b.Name] {
|
||||
return fmt.Errorf("duplicate build %q", b.Name)
|
||||
}
|
||||
seenB[b.Name] = true
|
||||
dst.Builds = append(dst.Builds, b)
|
||||
}
|
||||
|
||||
seenG := map[string]bool{}
|
||||
for _, g := range dst.Groups {
|
||||
seenG[g.Name] = true
|
||||
}
|
||||
for _, g := range src.Groups {
|
||||
if seenG[g.Name] {
|
||||
return fmt.Errorf("duplicate group %q", g.Name)
|
||||
}
|
||||
seenG[g.Name] = true
|
||||
dst.Groups = append(dst.Groups, g)
|
||||
}
|
||||
|
||||
if src.Timeout != 0 {
|
||||
dst.Timeout = src.Timeout
|
||||
}
|
||||
if src.MemoryLimit != 0 {
|
||||
dst.MemoryLimit = src.MemoryLimit
|
||||
}
|
||||
if src.Binary != "" {
|
||||
dst.Binary = src.Binary
|
||||
}
|
||||
if src.Sources != "" {
|
||||
dst.Sources = src.Sources
|
||||
}
|
||||
if src.NormalizeCRLF {
|
||||
dst.NormalizeCRLF = true
|
||||
}
|
||||
if src.TrimTrailingWS {
|
||||
dst.TrimTrailingWS = true
|
||||
}
|
||||
return nil
|
||||
}
|
||||
183
dsl/parser.go
183
dsl/parser.go
@@ -3,14 +3,18 @@ package dsl
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Parser struct {
|
||||
tokens []Token
|
||||
pos int
|
||||
warns []string
|
||||
tokens []Token
|
||||
pos int
|
||||
warns []string
|
||||
includeBaseDir string
|
||||
visited map[string]bool
|
||||
}
|
||||
|
||||
func NewParser(tokens []Token) *Parser {
|
||||
@@ -72,9 +76,67 @@ func Parse(src string) (*File, []string, error) {
|
||||
if err != nil {
|
||||
return nil, parser.Warnings(), err
|
||||
}
|
||||
if err := parser.finalize(file); err != nil {
|
||||
return nil, parser.Warnings(), err
|
||||
}
|
||||
return file, parser.Warnings(), nil
|
||||
}
|
||||
|
||||
func ParseFile(path string) (*File, []string, error) {
|
||||
abs, err := filepath.Abs(path)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("resolve %q: %w", path, err)
|
||||
}
|
||||
visited := map[string]bool{}
|
||||
parser, file, err := parseFileOnDisk(abs, visited)
|
||||
if err != nil {
|
||||
var warns []string
|
||||
if parser != nil {
|
||||
warns = parser.Warnings()
|
||||
}
|
||||
return nil, warns, err
|
||||
}
|
||||
if err := parser.finalize(file); err != nil {
|
||||
return nil, parser.Warnings(), err
|
||||
}
|
||||
return file, parser.Warnings(), nil
|
||||
}
|
||||
|
||||
func parseFileOnDisk(absPath string, visited map[string]bool) (*Parser, *File, error) {
|
||||
if visited[absPath] {
|
||||
return nil, nil, fmt.Errorf("circular include: %s", absPath)
|
||||
}
|
||||
visited[absPath] = true
|
||||
|
||||
src, err := os.ReadFile(absPath)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("read %s: %w", absPath, err)
|
||||
}
|
||||
tokens, err := NewLexer(string(src)).Tokenize()
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("%s: %w", absPath, err)
|
||||
}
|
||||
p := NewParser(tokens)
|
||||
p.includeBaseDir = filepath.Dir(absPath)
|
||||
p.visited = visited
|
||||
|
||||
file, err := p.parseFile()
|
||||
if err != nil {
|
||||
return p, nil, fmt.Errorf("%s: %w", absPath, err)
|
||||
}
|
||||
return p, file, nil
|
||||
}
|
||||
|
||||
func (p *Parser) finalize(f *File) error {
|
||||
if err := p.validateWeights(f); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := validateBuilds(f); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Parser) parseFile() (*File, error) {
|
||||
f := &File{}
|
||||
|
||||
@@ -91,7 +153,72 @@ func (p *Parser) parseFile() (*File, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
f.Build = s.Value
|
||||
if p.peek().Type == TOKEN_LBRACE {
|
||||
bc, err := p.parseBuildBlock(s.Value)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
f.Builds = append(f.Builds, bc)
|
||||
} else {
|
||||
f.Build = s.Value
|
||||
}
|
||||
|
||||
case "build_defaults":
|
||||
p.advance()
|
||||
bc, err := p.parseBuildBlock("")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if f.BuildDefaults == nil {
|
||||
f.BuildDefaults = &BuildConfig{}
|
||||
}
|
||||
f.BuildDefaults.MergeFrom(bc)
|
||||
|
||||
case "toolchains":
|
||||
p.advance()
|
||||
specs, err := p.parseToolchainsBlock()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
existing := map[string]bool{}
|
||||
for _, tc := range f.Toolchains {
|
||||
existing[tc.Name] = true
|
||||
}
|
||||
for _, tc := range specs {
|
||||
if existing[tc.Name] {
|
||||
return nil, fmt.Errorf("duplicate toolchain %q", tc.Name)
|
||||
}
|
||||
existing[tc.Name] = true
|
||||
f.Toolchains = append(f.Toolchains, tc)
|
||||
}
|
||||
|
||||
case "include":
|
||||
p.advance()
|
||||
s, err := p.expect(TOKEN_STRING)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if p.includeBaseDir == "" {
|
||||
return nil, fmt.Errorf("%d:%d: `include` requires file context (use ParseFile, not Parse)", s.Line, s.Col)
|
||||
}
|
||||
target := s.Value
|
||||
if !filepath.IsAbs(target) {
|
||||
target = filepath.Join(p.includeBaseDir, target)
|
||||
}
|
||||
abs, err := filepath.Abs(target)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%d:%d: resolve include %q: %w", s.Line, s.Col, s.Value, err)
|
||||
}
|
||||
childParser, child, err := parseFileOnDisk(abs, p.visited)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%d:%d: include %q: %w", s.Line, s.Col, s.Value, err)
|
||||
}
|
||||
for _, w := range childParser.Warnings() {
|
||||
p.warn(w)
|
||||
}
|
||||
if err := mergeFiles(f, child); err != nil {
|
||||
return nil, fmt.Errorf("%d:%d: include %q: %w", s.Line, s.Col, s.Value, err)
|
||||
}
|
||||
|
||||
case "build_linux":
|
||||
p.advance()
|
||||
@@ -192,11 +319,28 @@ func (p *Parser) parseFile() (*File, error) {
|
||||
}
|
||||
}
|
||||
|
||||
if err := p.validateWeights(f); err != nil {
|
||||
return nil, err
|
||||
return f, nil
|
||||
}
|
||||
|
||||
func validateBuilds(f *File) error {
|
||||
hasLegacy := f.Build != "" || f.BuildLinux != "" || f.BuildWindows != "" || f.BuildDarwin != ""
|
||||
hasStructured := f.BuildDefaults != nil || len(f.Builds) > 0
|
||||
|
||||
if hasLegacy && hasStructured {
|
||||
return fmt.Errorf("cannot mix legacy `build \"shell\"` with structured `build \"name\" { ... }` in the same suite")
|
||||
}
|
||||
|
||||
return f, nil
|
||||
seen := map[string]bool{}
|
||||
for _, b := range f.Builds {
|
||||
if b.Name == "" {
|
||||
return fmt.Errorf("structured build must have a name")
|
||||
}
|
||||
if seen[b.Name] {
|
||||
return fmt.Errorf("duplicate build name %q", b.Name)
|
||||
}
|
||||
seen[b.Name] = true
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Parser) validateWeights(f *File) error {
|
||||
@@ -635,25 +779,40 @@ func (p *Parser) parsePattern() (*Pattern, error) {
|
||||
if _, err := p.expect(TOKEN_ASSIGN); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
val, err := p.expect(TOKEN_STRING)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
switch t.Value {
|
||||
case "input":
|
||||
val, err := p.expect(TOKEN_STRING)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if pat.DirsGlob != "" {
|
||||
pat.InputFile = val.Value
|
||||
} else {
|
||||
pat.InputGlob = val.Value
|
||||
}
|
||||
case "output":
|
||||
val, err := p.expect(TOKEN_STRING)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if pat.DirsGlob != "" {
|
||||
pat.OutputFile = val.Value
|
||||
} else {
|
||||
pat.OutputGlob = val.Value
|
||||
}
|
||||
case "dirs":
|
||||
val, err := p.expect(TOKEN_STRING)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
pat.DirsGlob = val.Value
|
||||
case "args":
|
||||
xs, err := p.parseStringList()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
pat.Args = xs
|
||||
default:
|
||||
return nil, fmt.Errorf("%d:%d: unknown pattern field %q", t.Line, t.Col, t.Value)
|
||||
}
|
||||
@@ -715,8 +874,6 @@ func (p *Parser) parseInt() (int, error) {
|
||||
return n, nil
|
||||
}
|
||||
|
||||
// parseSize accepts either a TOKEN_SIZE (e.g. "256MB", "1GiB", "512K") or a bare
|
||||
// TOKEN_INT interpreted as bytes. MiB/MB are both 1024² — we use IEC semantics.
|
||||
func (p *Parser) parseSize() (int64, error) {
|
||||
t := p.peek()
|
||||
switch t.Type {
|
||||
|
||||
Reference in New Issue
Block a user