Skip to content

Commit 81a0b35

Browse files
akoclaude
andcommitted
fix: mxcli new fails on macOS with exec format error (Studio Pro lookup)
CDN mxbuild downloads are Linux ELF binaries. On macOS they fail with "exec format error" when used as the mx binary for create-project. Root causes: 1. ResolveStudioProDir() returned "" on all non-Windows platforms, so cmd_new.go always fell through to the CDN download path. 2. mendixSearchPaths() for darwin used /Applications/Mendix/*/modeler/ but Studio Pro on macOS installs to /Applications/Mendix Studio Pro X.Y.Z*.app/Contents/modeler/. 3. versionFromPath() couldn't extract the version from macOS .app paths. Fixes: - ResolveStudioProDir() now handles darwin via resolveStudioProDirMacOS(), which globs /Applications/Mendix Studio Pro *.app and extracts the base version (major.minor.patch) from the bundle name via regex, so "11.10.0-rc.7 Beta" correctly matches a request for "11.10.0". - mendixSearchPaths() darwin pattern updated to match real install layout. - versionFromPath() checks for macOS .app bundle paths first. - Adds ResolveMxForNewProject() helper that tries Studio Pro / cache before downloading; cmd_new.go uses it instead of inlining the logic. - Adds a helpful error message on macOS when no Studio Pro is found. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent dfa025c commit 81a0b35

6 files changed

Lines changed: 108 additions & 34 deletions

File tree

cmd/mxcli/cmd_new.go

Lines changed: 9 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -57,27 +57,17 @@ Examples:
5757
os.Exit(1)
5858
}
5959

60-
// Step 1: Resolve MxBuild and mx binary.
61-
// On Windows, prefer Studio Pro installation (ships both mxbuild.exe and mx.exe).
62-
// Fall back to CDN download on other platforms or when Studio Pro is not installed.
60+
// Step 1: Resolve mx binary.
61+
// On Windows and macOS, Studio Pro ships a native mx binary — prefer it.
62+
// CDN downloads contain Linux ELF binaries that cannot run on those platforms.
63+
// On Linux (CI, devcontainers), download mxbuild from CDN and derive mx.
6364
fmt.Printf("Step 1/4: Resolving MxBuild %s...\n", mendixVersion)
64-
var mxbuildPath string
65-
if studioDir := docker.ResolveStudioProDir(mendixVersion); studioDir != "" {
66-
mxbuildPath = filepath.Join(studioDir, "modeler", "mxbuild.exe")
67-
fmt.Printf(" Using Studio Pro: %s\n", studioDir)
68-
} else {
69-
var err error
70-
mxbuildPath, err = docker.DownloadMxBuild(mendixVersion, os.Stdout)
71-
if err != nil {
72-
fmt.Fprintf(os.Stderr, "Error downloading MxBuild: %v\n", err)
73-
os.Exit(1)
74-
}
75-
}
76-
77-
// Resolve mx binary from mxbuild path
78-
mxPath, err := docker.ResolveMx(mxbuildPath)
65+
mxPath, err := docker.ResolveMxForNewProject(mendixVersion, os.Stdout)
7966
if err != nil {
80-
fmt.Fprintf(os.Stderr, "Error: could not find mx binary: %v\n", err)
67+
fmt.Fprintf(os.Stderr, "Error: could not find mx binary for version %s: %v\n", mendixVersion, err)
68+
if runtime.GOOS == "darwin" {
69+
fmt.Fprintf(os.Stderr, " On macOS, install Mendix Studio Pro %s from the Mendix Marketplace.\n", mendixVersion)
70+
}
8171
os.Exit(1)
8272
}
8373

cmd/mxcli/docker/check.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,24 @@ func ResolveMxForVersion(mxbuildPath, preferredVersion string) (string, error) {
179179
return "", fmt.Errorf("mx not found; specify --mxbuild-path pointing to Mendix installation directory")
180180
}
181181

182+
// ResolveMxForNewProject finds the mx binary for use by mxcli new.
183+
// On Windows and macOS it prefers an installed Studio Pro to avoid downloading
184+
// Linux CDN binaries that won't execute on those platforms. On Linux (and as a
185+
// fallback) it downloads mxbuild from the CDN and derives mx from the same dir.
186+
func ResolveMxForNewProject(version string, progressWriter io.Writer) (string, error) {
187+
// Fast path: Studio Pro or cached download already present
188+
if mxPath, err := ResolveMxForVersion("", version); err == nil {
189+
return mxPath, nil
190+
}
191+
// Slow path: download mxbuild from CDN (works on Linux; on macOS/Windows
192+
// this is only reached if Studio Pro is not installed)
193+
mxbuildPath, err := DownloadMxBuild(version, progressWriter)
194+
if err != nil {
195+
return "", err
196+
}
197+
return ResolveMx(mxbuildPath)
198+
}
199+
182200
func CachedMxPath(version string) string {
183201
cacheDir, err := MxBuildCacheDir(version)
184202
if err != nil {

cmd/mxcli/docker/detect.go

Lines changed: 39 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,17 @@
99
// CDN downloads (mxbuild tar.gz) contain Linux ELF binaries that cannot
1010
// execute on Windows, so Studio Pro installations MUST be preferred.
1111
//
12-
// On Linux/macOS (CI, devcontainers), Studio Pro is not available.
12+
// On macOS, Studio Pro installs to "/Applications/Mendix Studio Pro X.Y.Z.app/".
13+
// CDN downloads are Linux ELF binaries and also cannot run on macOS.
14+
// Studio Pro installations are therefore preferred on macOS as well.
15+
//
16+
// On Linux (CI, devcontainers), Studio Pro is not available.
1317
// CDN downloads are the primary source for mxbuild and runtime.
1418
//
1519
// Resolution priority (all platforms):
1620
// 1. Explicit path (--mxbuild-path)
1721
// 2. PATH lookup
18-
// 3. OS-specific known locations (Studio Pro on Windows)
22+
// 3. OS-specific known locations (Studio Pro on Windows and macOS)
1923
// 4. Cached CDN downloads (~/.mxcli/mxbuild/)
2024
//
2125
// Path discovery on Windows must NOT hardcode drive letters. Use environment
@@ -136,16 +140,40 @@ func resolveMxBuild(explicitPath string, preferredVersion ...string) (string, er
136140
}
137141

138142
// ResolveStudioProDir finds the Studio Pro installation directory for a specific
139-
// Mendix version on Windows. Returns the installation root (e.g.,
140-
// "D:\Program Files\Mendix\11.6.4") or empty string if not found.
141-
// On non-Windows platforms, always returns empty string.
143+
// Mendix version. Returns the installation root on Windows (e.g.,
144+
// "D:\Program Files\Mendix\11.6.4") or the app Contents directory on macOS
145+
// (e.g., "/Applications/Mendix Studio Pro 11.10.0.app/Contents").
146+
// Returns empty string if not found or on Linux.
142147
func ResolveStudioProDir(version string) string {
143-
if runtime.GOOS != "windows" {
144-
return ""
148+
switch runtime.GOOS {
149+
case "windows":
150+
for _, dir := range windowsProgramDirs() {
151+
candidate := filepath.Join(dir, "Mendix", version)
152+
if info, err := os.Stat(filepath.Join(candidate, "modeler", "mxbuild.exe")); err == nil && !info.IsDir() {
153+
return candidate
154+
}
155+
}
156+
case "darwin":
157+
return resolveStudioProDirMacOS(version)
145158
}
146-
for _, dir := range windowsProgramDirs() {
147-
candidate := filepath.Join(dir, "Mendix", version)
148-
if info, err := os.Stat(filepath.Join(candidate, "modeler", "mxbuild.exe")); err == nil && !info.IsDir() {
159+
return ""
160+
}
161+
162+
// resolveStudioProDirMacOS finds an installed Studio Pro on macOS that matches
163+
// the requested version. App bundles are named "Mendix Studio Pro X.Y.Z*.app"
164+
// where X.Y.Z is the base version (e.g., "11.10.0-rc.7 Beta" for 11.10.0).
165+
// Returns the bundle's Contents directory, or "" if not found.
166+
func resolveStudioProDirMacOS(version string) string {
167+
matches, _ := filepath.Glob("/Applications/Mendix Studio Pro *.app")
168+
re := regexp.MustCompile(`^Mendix Studio Pro (\d+\.\d+\.\d+)`)
169+
for _, match := range matches {
170+
base := strings.TrimSuffix(filepath.Base(match), ".app")
171+
m := re.FindStringSubmatch(base)
172+
if m == nil || m[1] != version {
173+
continue
174+
}
175+
candidate := filepath.Join(match, "Contents")
176+
if _, err := os.Stat(filepath.Join(candidate, "modeler", "mx")); err == nil {
149177
return candidate
150178
}
151179
}
@@ -189,7 +217,7 @@ func mendixSearchPaths(binaryName string) []string {
189217
}
190218
return paths
191219
case "darwin":
192-
return []string{filepath.Join("/Applications/Mendix/*/modeler", binaryName)}
220+
return []string{"/Applications/Mendix Studio Pro *.app/Contents/modeler/" + binaryName}
193221
default: // linux
194222
paths := []string{filepath.Join("/opt/mendix/*/modeler", binaryName)}
195223
if home, err := os.UserHomeDir(); err == nil {

cmd/mxcli/docker/detect_test.go

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -197,12 +197,24 @@ func TestJdkSearchPaths_NoHardcodedDriveLetter(t *testing.T) {
197197
}
198198
}
199199

200-
func TestResolveStudioProDir_NotWindows(t *testing.T) {
201-
if runtime.GOOS == "windows" {
202-
t.Skip("non-windows test")
200+
func TestResolveStudioProDir_Linux(t *testing.T) {
201+
if runtime.GOOS != "linux" {
202+
t.Skip("linux-only test")
203203
}
204204
if dir := ResolveStudioProDir("11.6.4"); dir != "" {
205-
t.Errorf("expected empty on non-windows, got %s", dir)
205+
t.Errorf("expected empty on linux, got %s", dir)
206+
}
207+
}
208+
209+
func TestResolveStudioProDir_MacOS_FakeInstall(t *testing.T) {
210+
if runtime.GOOS != "darwin" {
211+
t.Skip("macOS-only test")
212+
}
213+
// Simulate a Studio Pro .app bundle in a temp Applications directory.
214+
// resolveStudioProDirMacOS is hard-coded to /Applications, so we test
215+
// it indirectly by confirming that a non-existent version returns "".
216+
if dir := resolveStudioProDirMacOS("0.0.0"); dir != "" {
217+
t.Errorf("expected empty for non-existent version, got %s", dir)
206218
}
207219
}
208220

cmd/mxcli/docker/download.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
"path/filepath"
1313
"runtime"
1414
"strconv"
15+
"regexp"
1516
"strings"
1617
)
1718

@@ -137,6 +138,10 @@ func NewestVersionedPath(paths []string) string {
137138
}
138139

139140
func versionFromPath(path string) string {
141+
// macOS Studio Pro bundles: .../Mendix Studio Pro X.Y.Z*.app/Contents/modeler/<binary>
142+
if m := regexp.MustCompile(`Mendix Studio Pro (\d+\.\d+\.\d+)`).FindStringSubmatch(path); m != nil {
143+
return m[1]
144+
}
140145
versionDir := filepath.Dir(filepath.Dir(path))
141146
return filepath.Base(versionDir)
142147
}

cmd/mxcli/docker/download_test.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,3 +194,24 @@ func TestResolveMxBuild_FindsCachedVersion(t *testing.T) {
194194
}
195195
}
196196
}
197+
198+
func TestVersionFromPath_MacOSAppBundle(t *testing.T) {
199+
path := "/Applications/Mendix Studio Pro 11.10.0.app/Contents/modeler/mx"
200+
if got := versionFromPath(path); got != "11.10.0" {
201+
t.Errorf("versionFromPath(%q) = %q; want %q", path, got, "11.10.0")
202+
}
203+
}
204+
205+
func TestVersionFromPath_MacOSAppBundle_RC(t *testing.T) {
206+
path := "/Applications/Mendix Studio Pro 11.10.0-rc.7 Beta.app/Contents/modeler/mx"
207+
if got := versionFromPath(path); got != "11.10.0" {
208+
t.Errorf("versionFromPath(%q) = %q; want %q", path, got, "11.10.0")
209+
}
210+
}
211+
212+
func TestVersionFromPath_CachedLinux(t *testing.T) {
213+
path := "/home/user/.mxcli/mxbuild/11.9.0/modeler/mxbuild"
214+
if got := versionFromPath(path); got != "11.9.0" {
215+
t.Errorf("versionFromPath(%q) = %q; want %q", path, got, "11.9.0")
216+
}
217+
}

0 commit comments

Comments
 (0)