Skip to content

Commit 1a22f90

Browse files
committed
merge: submit/integration-test-stabilization (PR mendixlabs#267)
# Conflicts: # mdl/executor/cmd_microflows_create.go # mdl/executor/executor.go
2 parents 1ed6a0e + 912e0f9 commit 1a22f90

37 files changed

Lines changed: 1277 additions & 130 deletions

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,7 @@ test-integration:
151151
# Usage: make test-mdl MPR=path/to/app.mpr
152152
MPR ?= app.mpr
153153
test-mdl: build
154-
$(BUILD_DIR)/$(BINARY_NAME) test mdl-examples/doctype-tests/microflow-spec.test.mdl -p $(MPR)
154+
./scripts/run-mdl-tests.sh "$(abspath $(MPR))" "$(abspath $(BUILD_DIR)/$(BINARY_NAME))"
155155

156156
# Lint all code (Go + TypeScript)
157157
lint: lint-go lint-ts

cmd/mxcli/docker/build.go

Lines changed: 38 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ func Build(opts BuildOptions) error {
6565

6666
// Step 2: Resolve MxBuild
6767
fmt.Fprintln(w, "Resolving MxBuild...")
68-
mxbuildPath, err := resolveMxBuild(opts.MxBuildPath)
68+
mxbuildPath, err := resolveMxBuild(opts.MxBuildPath, pv.ProductVersion)
6969
if err != nil {
7070
// Auto-download fallback
7171
fmt.Fprintln(w, " MxBuild not found locally, downloading from CDN...")
@@ -96,7 +96,7 @@ func Build(opts BuildOptions) error {
9696
// Step 4: Pre-build check
9797
if !opts.SkipCheck {
9898
fmt.Fprintln(w, "Checking project for errors...")
99-
mxPath, err := ResolveMx(opts.MxBuildPath)
99+
mxPath, err := ResolveMxForVersion(opts.MxBuildPath, pv.ProductVersion)
100100
if err != nil {
101101
fmt.Fprintf(w, " Skipping check: %v\n", err)
102102
} else {
@@ -383,8 +383,10 @@ func Run(opts RunOptions) error {
383383
}
384384

385385
// findPADDir searches for a PAD output in the output directory tree.
386-
// It looks for a Dockerfile first, then for docker_compose/Default.yaml as a fallback
387-
// (MxBuild 11.6.3+ generates docker_compose instead of a Dockerfile).
386+
// It recognizes:
387+
// - classic PAD output with a Dockerfile
388+
// - PAD output with docker_compose/Default.yaml
389+
// - newer PAD output already extracted at the build root (app/bin/etc/lib)
388390
func findPADDir(outputDir string) (string, error) {
389391
// Check output dir itself
390392
if isPADDir(outputDir) {
@@ -405,19 +407,48 @@ func findPADDir(outputDir string) (string, error) {
405407
}
406408
}
407409

408-
return "", fmt.Errorf("no PAD output found in %s (looked for Dockerfile or docker_compose/Default.yaml)", outputDir)
410+
return "", fmt.Errorf("no PAD output found in %s (looked for Dockerfile, docker_compose/Default.yaml, or extracted app/bin/etc/lib layout)", outputDir)
409411
}
410412

411413
// isPADDir checks if a directory contains PAD output.
412-
// Recognizes both Dockerfile-based PAD and docker_compose-based PAD.
414+
// Recognizes Dockerfile-based PAD, docker_compose-based PAD, and newer
415+
// already-extracted PAD layout at the build root.
413416
func isPADDir(dir string) bool {
414417
if _, err := os.Stat(filepath.Join(dir, "Dockerfile")); err == nil {
415418
return true
416419
}
417420
if _, err := os.Stat(filepath.Join(dir, "docker_compose", "Default.yaml")); err == nil {
418421
return true
419422
}
420-
return false
423+
return hasExtractedPADLayout(dir)
424+
}
425+
426+
func hasExtractedPADLayout(dir string) bool {
427+
requiredDirs := []string{
428+
filepath.Join(dir, "app"),
429+
filepath.Join(dir, "bin"),
430+
filepath.Join(dir, "etc"),
431+
filepath.Join(dir, "lib"),
432+
}
433+
for _, path := range requiredDirs {
434+
info, err := os.Stat(path)
435+
if err != nil || !info.IsDir() {
436+
return false
437+
}
438+
}
439+
440+
requiredFiles := []string{
441+
filepath.Join(dir, "bin", "start"),
442+
filepath.Join(dir, "lib", "runtime", "launcher", "runtimelauncher.jar"),
443+
}
444+
for _, path := range requiredFiles {
445+
info, err := os.Stat(path)
446+
if err != nil || info.IsDir() {
447+
return false
448+
}
449+
}
450+
451+
return true
421452
}
422453

423454
// flattenPADDir moves contents from a PAD subdirectory to the output root directory.

cmd/mxcli/docker/build_test.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -434,6 +434,34 @@ func TestFindPADDir_DockerCompose_InSubdir(t *testing.T) {
434434
}
435435
}
436436

437+
func TestFindPADDir_ExtractedLayoutInRoot(t *testing.T) {
438+
dir := t.TempDir()
439+
for _, subdir := range []string{
440+
filepath.Join(dir, "app"),
441+
filepath.Join(dir, "etc"),
442+
filepath.Join(dir, "bin"),
443+
filepath.Join(dir, "lib", "runtime", "launcher"),
444+
} {
445+
if err := os.MkdirAll(subdir, 0755); err != nil {
446+
t.Fatalf("mkdir %s: %v", subdir, err)
447+
}
448+
}
449+
if err := os.WriteFile(filepath.Join(dir, "bin", "start"), []byte("#!/bin/sh\n"), 0755); err != nil {
450+
t.Fatalf("write start script: %v", err)
451+
}
452+
if err := os.WriteFile(filepath.Join(dir, "lib", "runtime", "launcher", "runtimelauncher.jar"), []byte("fake"), 0644); err != nil {
453+
t.Fatalf("write launcher: %v", err)
454+
}
455+
456+
padDir, err := findPADDir(dir)
457+
if err != nil {
458+
t.Fatalf("unexpected error: %v", err)
459+
}
460+
if padDir != dir {
461+
t.Errorf("expected %s, got %s", dir, padDir)
462+
}
463+
}
464+
437465
func TestFindPADDir_PrefersDockerfile(t *testing.T) {
438466
dir := t.TempDir()
439467
// Both Dockerfile and docker_compose exist — Dockerfile wins

cmd/mxcli/docker/check.go

Lines changed: 60 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ import (
1010
"path/filepath"
1111
"runtime"
1212
"strings"
13+
14+
"github.com/mendixlabs/mxcli/sdk/mpr"
1315
)
1416

1517
// CheckOptions configures the mx check command.
@@ -44,7 +46,15 @@ func Check(opts CheckOptions) error {
4446
}
4547

4648
// Resolve mx binary
47-
mxPath, err := ResolveMx(opts.MxBuildPath)
49+
projectVersion := ""
50+
if opts.ProjectPath != "" {
51+
if reader, err := mpr.Open(opts.ProjectPath); err == nil {
52+
projectVersion = reader.ProjectVersion().ProductVersion
53+
reader.Close()
54+
}
55+
}
56+
57+
mxPath, err := ResolveMxForVersion(opts.MxBuildPath, projectVersion)
4858
if err != nil {
4959
return err
5060
}
@@ -88,12 +98,25 @@ func mxBinaryName() string {
8898
return "mx"
8999
}
90100

101+
func mxBinaryNames() []string {
102+
if runtime.GOOS == "windows" {
103+
return []string{"mx.exe", "mx"}
104+
}
105+
return []string{"mx"}
106+
}
107+
91108
// ResolveMx finds the mx executable.
92109
// Priority: derive from mxbuild path > PATH lookup.
93110
func ResolveMx(mxbuildPath string) (string, error) {
111+
return ResolveMxForVersion(mxbuildPath, "")
112+
}
113+
114+
// ResolveMxForVersion finds the mx executable, preferring the project's exact
115+
// Mendix version when multiple local installations or cached downloads exist.
116+
func ResolveMxForVersion(mxbuildPath, preferredVersion string) (string, error) {
94117
if mxbuildPath != "" {
95118
// Resolve mxbuild first to handle directory paths
96-
resolvedMxBuild, err := resolveMxBuild(mxbuildPath)
119+
resolvedMxBuild, err := resolveMxBuild(mxbuildPath, preferredVersion)
97120
if err == nil {
98121
// Look for mx in the same directory as mxbuild
99122
mxDir := filepath.Dir(resolvedMxBuild)
@@ -123,24 +146,47 @@ func ResolveMx(mxbuildPath string) (string, error) {
123146
return p, nil
124147
}
125148

149+
if preferredVersion != "" {
150+
if studioProDir := ResolveStudioProDir(preferredVersion); studioProDir != "" {
151+
for _, name := range mxBinaryNames() {
152+
candidate := filepath.Join(studioProDir, "modeler", name)
153+
if info, err := os.Stat(candidate); err == nil && !info.IsDir() {
154+
return candidate, nil
155+
}
156+
}
157+
}
158+
}
159+
126160
// Try OS-specific known locations (Studio Pro on Windows) before cached downloads.
127-
for _, pattern := range mendixSearchPaths(mxBinaryName()) {
128-
matches, _ := filepath.Glob(pattern)
129-
if len(matches) > 0 {
130-
return matches[len(matches)-1], nil
161+
if matches := globVersionedMatches(mendixSearchPaths(mxBinaryName())); len(matches) > 0 {
162+
if exact := exactVersionedPath(matches, preferredVersion); exact != "" {
163+
return exact, nil
164+
}
165+
if newest := newestVersionedPath(matches); newest != "" {
166+
return newest, nil
131167
}
132168
}
133169

134-
// Try cached mxbuild installations (~/.mxcli/mxbuild/*/modeler/mx).
135-
// NOTE: lexicographic sort is imperfect for versions (e.g. "9.x" > "10.x"),
136-
// but this is a fallback-of-last-resort — in practice users typically have
137-
// only one mxbuild version installed.
138-
if home, err := os.UserHomeDir(); err == nil {
139-
matches, _ := filepath.Glob(filepath.Join(home, ".mxcli", "mxbuild", "*", "modeler", mxBinaryName()))
140-
if len(matches) > 0 {
141-
return matches[len(matches)-1], nil
170+
if preferredVersion != "" {
171+
if p := CachedMxPath(preferredVersion); p != "" {
172+
return p, nil
142173
}
143174
}
175+
if p := AnyCachedMxPath(); p != "" {
176+
return p, nil
177+
}
144178

145179
return "", fmt.Errorf("mx not found; specify --mxbuild-path pointing to Mendix installation directory")
146180
}
181+
182+
func CachedMxPath(version string) string {
183+
cacheDir, err := MxBuildCacheDir(version)
184+
if err != nil {
185+
return ""
186+
}
187+
return cachedBinaryPath(cacheDir, mxBinaryNames())
188+
}
189+
190+
func AnyCachedMxPath() string {
191+
return anyCachedBinaryPath(mxBinaryNames())
192+
}

cmd/mxcli/docker/check_test.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,3 +118,35 @@ func TestCheck_SkipUpdateWidgetsFlag(t *testing.T) {
118118
t.Error("check should still be called")
119119
}
120120
}
121+
122+
func TestResolveMxForVersion_PrefersExactCachedVersion(t *testing.T) {
123+
dir := t.TempDir()
124+
setTestHomeDir(t, dir)
125+
// Point PATH at an empty temp dir (rather than clearing it) so exec.LookPath
126+
// still works for any other testing infrastructure but can't find mx.
127+
t.Setenv("PATH", t.TempDir())
128+
129+
versions := []string{"9.24.40.80973", "11.6.3", "11.9.0"}
130+
var expected string
131+
for _, version := range versions {
132+
modelerDir := filepath.Join(dir, ".mxcli", "mxbuild", version, "modeler")
133+
if err := os.MkdirAll(modelerDir, 0755); err != nil {
134+
t.Fatal(err)
135+
}
136+
bin := filepath.Join(modelerDir, mxBinaryName())
137+
if err := os.WriteFile(bin, []byte("fake"), 0755); err != nil {
138+
t.Fatal(err)
139+
}
140+
if version == "11.9.0" {
141+
expected = bin
142+
}
143+
}
144+
145+
result, err := ResolveMxForVersion("", "11.9.0")
146+
if err != nil {
147+
t.Fatalf("unexpected error: %v", err)
148+
}
149+
if result != expected {
150+
t.Errorf("expected exact cached mx %s, got %s", expected, result)
151+
}
152+
}

cmd/mxcli/docker/detect.go

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,12 @@ func findMxBuildInDir(dir string) string {
7373
// CDN downloads are Linux binaries that cannot run natively on Windows.
7474
// The explicit path can be the binary itself or a directory containing it
7575
// (e.g., a Mendix installation root with modeler/mxbuild inside).
76-
func resolveMxBuild(explicitPath string) (string, error) {
76+
func resolveMxBuild(explicitPath string, preferredVersion ...string) (string, error) {
77+
targetVersion := ""
78+
if len(preferredVersion) > 0 {
79+
targetVersion = preferredVersion[0]
80+
}
81+
7782
if explicitPath != "" {
7883
info, err := os.Stat(explicitPath)
7984
if err != nil {
@@ -94,13 +99,31 @@ func resolveMxBuild(explicitPath string) (string, error) {
9499
return p, nil
95100
}
96101

102+
// Try the exact Studio Pro installation for the project version first.
103+
// This keeps Docker builds on the same Mendix line as the project when
104+
// multiple installs are present locally.
105+
if targetVersion != "" {
106+
if studioProDir := ResolveStudioProDir(targetVersion); studioProDir != "" {
107+
if found := findMxBuildInDir(studioProDir); found != "" {
108+
return found, nil
109+
}
110+
}
111+
}
112+
97113
// Try OS-specific known locations (Studio Pro on Windows) BEFORE cached downloads.
98114
// On Windows, CDN downloads are Linux binaries — Studio Pro's mxbuild.exe is preferred.
99-
for _, pattern := range mxbuildSearchPaths() {
100-
matches, _ := filepath.Glob(pattern)
101-
if len(matches) > 0 {
102-
// Return the last match (likely newest version)
103-
return matches[len(matches)-1], nil
115+
if matches := globVersionedMatches(mxbuildSearchPaths()); len(matches) > 0 {
116+
if exact := exactVersionedPath(matches, targetVersion); exact != "" {
117+
return exact, nil
118+
}
119+
if newest := newestVersionedPath(matches); newest != "" {
120+
return newest, nil
121+
}
122+
}
123+
124+
if targetVersion != "" {
125+
if p := CachedMxBuildPath(targetVersion); p != "" {
126+
return p, nil
104127
}
105128
}
106129

cmd/mxcli/docker/detect_test.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -268,3 +268,35 @@ func TestResolveMxBuild_PrefersStudioProOverCache(t *testing.T) {
268268
t.Errorf("expected Studio Pro binary %s, got %s (should prefer Studio Pro over cache)", studioBin, result)
269269
}
270270
}
271+
272+
func TestResolveMxBuild_PrefersExactCachedVersion(t *testing.T) {
273+
dir := t.TempDir()
274+
setTestHomeDir(t, dir)
275+
// Point PATH at an empty temp dir (rather than clearing it) so exec.LookPath
276+
// still works for any other testing infrastructure but can't find mxbuild.
277+
t.Setenv("PATH", t.TempDir())
278+
279+
versions := []string{"9.24.40.80973", "11.6.3", "11.9.0"}
280+
var expected string
281+
for _, version := range versions {
282+
modelerDir := filepath.Join(dir, ".mxcli", "mxbuild", version, "modeler")
283+
if err := os.MkdirAll(modelerDir, 0755); err != nil {
284+
t.Fatal(err)
285+
}
286+
bin := filepath.Join(modelerDir, mxbuildBinaryName())
287+
if err := os.WriteFile(bin, []byte("fake"), 0755); err != nil {
288+
t.Fatal(err)
289+
}
290+
if version == "11.9.0" {
291+
expected = bin
292+
}
293+
}
294+
295+
result, err := resolveMxBuild("", "11.9.0")
296+
if err != nil {
297+
t.Fatalf("unexpected error: %v", err)
298+
}
299+
if result != expected {
300+
t.Errorf("expected exact cached version %s, got %s", expected, result)
301+
}
302+
}

0 commit comments

Comments
 (0)