Skip to content

Commit 36378fa

Browse files
engalarclaude
andcommitted
fix: prefer Studio Pro installation over CDN downloads on Windows
CDN mxbuild downloads are Linux ELF binaries that cannot execute on Windows. Reorder resolution priority so Studio Pro installations are found before cached downloads. Unify path discovery through windowsProgramDirs() to eliminate hardcoded drive letters — paths are now derived from PROGRAMFILES/PROGRAMW6432/SystemDrive env vars. Also fixes filepath.Join bug with SystemDrive ("D:" without separator produced relative paths like "D:Program Files"). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 20c8d87 commit 36378fa

File tree

5 files changed

+318
-66
lines changed

5 files changed

+318
-66
lines changed

cmd/mxcli/cmd_new.go

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -57,12 +57,21 @@ Examples:
5757
os.Exit(1)
5858
}
5959

60-
// Step 1: Download MxBuild
61-
fmt.Printf("Step 1/4: Downloading MxBuild %s...\n", mendixVersion)
62-
mxbuildPath, err := docker.DownloadMxBuild(mendixVersion, os.Stdout)
63-
if err != nil {
64-
fmt.Fprintf(os.Stderr, "Error downloading MxBuild: %v\n", err)
65-
os.Exit(1)
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.
63+
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+
}
6675
}
6776

6877
// Resolve mx binary from mxbuild path

cmd/mxcli/docker/build.go

Lines changed: 34 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -76,9 +76,12 @@ func Build(opts BuildOptions) error {
7676
}
7777
fmt.Fprintf(w, " MxBuild: %s\n", mxbuildPath)
7878

79-
// Step 2b: Ensure PAD runtime files are linked
80-
if err := ensurePADFiles(pv.ProductVersion, w); err != nil {
81-
fmt.Fprintf(w, " Warning: %v\n", err)
79+
// Step 2b: Ensure PAD runtime files are linked (only needed for CDN downloads,
80+
// Studio Pro installations already have runtime files in place).
81+
if ResolveStudioProDir(pv.ProductVersion) == "" {
82+
if err := ensurePADFiles(pv.ProductVersion, w); err != nil {
83+
fmt.Fprintf(w, " Warning: %v\n", err)
84+
}
8285
}
8386

8487
// Step 3: Resolve JDK 21
@@ -278,27 +281,36 @@ func Run(opts RunOptions) error {
278281
reader.Close()
279282
fmt.Fprintf(w, " Mendix version: %s\n", pv.ProductVersion)
280283

281-
// Step 2: Ensure MxBuild is available
282-
fmt.Fprintln(w, "Ensuring MxBuild is available...")
283-
_, err = DownloadMxBuild(pv.ProductVersion, w)
284-
if err != nil {
285-
return fmt.Errorf("setting up mxbuild: %w", err)
286-
}
284+
// Step 2 & 3: Ensure MxBuild and runtime are available.
285+
// On Windows, prefer Studio Pro installation directory — it ships both mxbuild.exe
286+
// and runtime/, so no CDN download is needed.
287+
studioProDir := ResolveStudioProDir(pv.ProductVersion)
288+
if studioProDir != "" {
289+
fmt.Fprintf(w, "Using Studio Pro installation: %s\n", studioProDir)
290+
// Point MxBuildPath so Build() uses Studio Pro's mxbuild.exe
291+
if opts.MxBuildPath == "" {
292+
opts.MxBuildPath = studioProDir
293+
}
294+
} else {
295+
fmt.Fprintln(w, "Ensuring MxBuild is available...")
296+
_, err = DownloadMxBuild(pv.ProductVersion, w)
297+
if err != nil {
298+
return fmt.Errorf("setting up mxbuild: %w", err)
299+
}
287300

288-
// Step 3: Ensure runtime is available
289-
fmt.Fprintln(w, "Ensuring Mendix runtime is available...")
290-
_, err = DownloadRuntime(pv.ProductVersion, w)
291-
if err != nil {
292-
return fmt.Errorf("setting up runtime: %w", err)
293-
}
301+
fmt.Fprintln(w, "Ensuring Mendix runtime is available...")
302+
_, err = DownloadRuntime(pv.ProductVersion, w)
303+
if err != nil {
304+
return fmt.Errorf("setting up runtime: %w", err)
305+
}
294306

295-
// Step 3b: Link PAD runtime files into mxbuild directory
296-
// MxBuild's PAD builder expects template files at mxbuild/{ver}/runtime/pad/,
297-
// but they live in the separately downloaded runtime at runtime/{ver}/runtime/pad/.
298-
// Non-fatal: some Mendix versions don't include PAD files in the runtime archive.
299-
// The docker build step handles this gracefully by downloading the runtime separately.
300-
if err := ensurePADFiles(pv.ProductVersion, w); err != nil {
301-
fmt.Fprintf(w, " Warning: %v\n", err)
307+
// Link PAD runtime files into mxbuild directory.
308+
// MxBuild's PAD builder expects template files at mxbuild/{ver}/runtime/pad/,
309+
// but they live in the separately downloaded runtime at runtime/{ver}/runtime/pad/.
310+
// Non-fatal: some Mendix versions don't include PAD files in the runtime archive.
311+
if err := ensurePADFiles(pv.ProductVersion, w); err != nil {
312+
fmt.Fprintf(w, " Warning: %v\n", err)
313+
}
302314
}
303315

304316
// Step 3c: Ensure demo users exist

cmd/mxcli/docker/check.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,14 @@ func ResolveMx(mxbuildPath string) (string, error) {
123123
return p, nil
124124
}
125125

126+
// 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
131+
}
132+
}
133+
126134
// Try cached mxbuild installations (~/.mxcli/mxbuild/*/modeler/mx).
127135
// NOTE: lexicographic sort is imperfect for versions (e.g. "9.x" > "10.x"),
128136
// but this is a fallback-of-last-resort — in practice users typically have

cmd/mxcli/docker/detect.go

Lines changed: 92 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,25 @@
11
// SPDX-License-Identifier: Apache-2.0
22

33
// Package docker implements Docker build and deployment support for Mendix projects.
4+
//
5+
// # Platform differences for Mendix tool resolution
6+
//
7+
// On Windows, Studio Pro installs mxbuild.exe, mx.exe, and the runtime under
8+
// a Program Files directory (e.g., D:\Program Files\Mendix\11.6.4\).
9+
// CDN downloads (mxbuild tar.gz) contain Linux ELF binaries that cannot
10+
// execute on Windows, so Studio Pro installations MUST be preferred.
11+
//
12+
// On Linux/macOS (CI, devcontainers), Studio Pro is not available.
13+
// CDN downloads are the primary source for mxbuild and runtime.
14+
//
15+
// Resolution priority (all platforms):
16+
// 1. Explicit path (--mxbuild-path)
17+
// 2. PATH lookup
18+
// 3. OS-specific known locations (Studio Pro on Windows)
19+
// 4. Cached CDN downloads (~/.mxcli/mxbuild/)
20+
//
21+
// Path discovery on Windows must NOT hardcode drive letters. Use environment
22+
// variables (PROGRAMFILES, PROGRAMW6432, SystemDrive) to locate install dirs.
423
package docker
524

625
import (
@@ -49,7 +68,9 @@ func findMxBuildInDir(dir string) string {
4968
}
5069

5170
// resolveMxBuild finds the MxBuild executable.
52-
// Priority: explicit path > PATH lookup > OS-specific known locations.
71+
// Priority: explicit path > PATH lookup > OS-specific known locations > cached downloads.
72+
// On Windows, Studio Pro installations are checked before cached downloads because
73+
// CDN downloads are Linux binaries that cannot run natively on Windows.
5374
// The explicit path can be the binary itself or a directory containing it
5475
// (e.g., a Mendix installation root with modeler/mxbuild inside).
5576
func resolveMxBuild(explicitPath string) (string, error) {
@@ -73,12 +94,8 @@ func resolveMxBuild(explicitPath string) (string, error) {
7394
return p, nil
7495
}
7596

76-
// Try cached downloads (~/.mxcli/mxbuild/*/modeler/mxbuild)
77-
if p := AnyCachedMxBuildPath(); p != "" {
78-
return p, nil
79-
}
80-
81-
// Try OS-specific known locations
97+
// Try OS-specific known locations (Studio Pro on Windows) BEFORE cached downloads.
98+
// On Windows, CDN downloads are Linux binaries — Studio Pro's mxbuild.exe is preferred.
8299
for _, pattern := range mxbuildSearchPaths() {
83100
matches, _ := filepath.Glob(pattern)
84101
if len(matches) > 0 {
@@ -87,41 +104,83 @@ func resolveMxBuild(explicitPath string) (string, error) {
87104
}
88105
}
89106

107+
// Try cached downloads (~/.mxcli/mxbuild/*/modeler/mxbuild)
108+
if p := AnyCachedMxBuildPath(); p != "" {
109+
return p, nil
110+
}
111+
90112
return "", fmt.Errorf("mxbuild not found; install Mendix Studio Pro or specify --mxbuild-path")
91113
}
92114

93-
// mxbuildSearchPaths returns OS-specific glob patterns for MxBuild.
94-
func mxbuildSearchPaths() []string {
115+
// ResolveStudioProDir finds the Studio Pro installation directory for a specific
116+
// Mendix version on Windows. Returns the installation root (e.g.,
117+
// "D:\Program Files\Mendix\11.6.4") or empty string if not found.
118+
// On non-Windows platforms, always returns empty string.
119+
func ResolveStudioProDir(version string) string {
120+
if runtime.GOOS != "windows" {
121+
return ""
122+
}
123+
for _, dir := range windowsProgramDirs() {
124+
candidate := filepath.Join(dir, "Mendix", version)
125+
if info, err := os.Stat(filepath.Join(candidate, "modeler", "mxbuild.exe")); err == nil && !info.IsDir() {
126+
return candidate
127+
}
128+
}
129+
return ""
130+
}
131+
132+
// windowsProgramDirs returns candidate Program Files directories on Windows,
133+
// derived from environment variables and the system drive letter.
134+
func windowsProgramDirs() []string {
135+
seen := map[string]bool{}
136+
var dirs []string
137+
add := func(d string) {
138+
if d != "" && !seen[d] {
139+
seen[d] = true
140+
dirs = append(dirs, d)
141+
}
142+
}
143+
for _, env := range []string{"PROGRAMFILES", "PROGRAMW6432", "PROGRAMFILES(X86)"} {
144+
add(os.Getenv(env))
145+
}
146+
// Fallback: derive from SystemDrive (e.g., "D:\Program Files").
147+
// SystemDrive returns "D:" without a trailing separator; filepath.Join
148+
// treats "D:" as a relative path, producing "D:Program Files" instead of
149+
// "D:\Program Files". Append the separator explicitly.
150+
if sysDrive := os.Getenv("SystemDrive"); sysDrive != "" {
151+
root := sysDrive + string(os.PathSeparator)
152+
add(filepath.Join(root, "Program Files"))
153+
add(filepath.Join(root, "Program Files (x86)"))
154+
}
155+
return dirs
156+
}
157+
158+
// mendixSearchPaths returns OS-specific glob patterns for a Mendix binary
159+
// (e.g., "mxbuild.exe", "mx.exe", "mxbuild", "mx") inside Studio Pro installations.
160+
func mendixSearchPaths(binaryName string) []string {
95161
switch runtime.GOOS {
96162
case "windows":
97163
var paths []string
98-
// Use environment variables — the system drive is not always C:.
99-
for _, env := range []string{"PROGRAMFILES", "PROGRAMW6432", "PROGRAMFILES(X86)"} {
100-
if dir := os.Getenv(env); dir != "" {
101-
paths = append(paths, filepath.Join(dir, "Mendix", "*", "modeler", "mxbuild.exe"))
102-
}
103-
}
104-
if len(paths) == 0 {
105-
// Fallback if env vars are missing (unlikely but safe).
106-
paths = []string{`C:\Program Files\Mendix\*\modeler\mxbuild.exe`}
164+
for _, dir := range windowsProgramDirs() {
165+
paths = append(paths, filepath.Join(dir, "Mendix", "*", "modeler", binaryName))
107166
}
108167
return paths
109168
case "darwin":
110-
return []string{
111-
"/Applications/Mendix/*/modeler/mxbuild",
112-
}
169+
return []string{filepath.Join("/Applications/Mendix/*/modeler", binaryName)}
113170
default: // linux
114-
home, _ := os.UserHomeDir()
115-
paths := []string{
116-
"/opt/mendix/*/modeler/mxbuild",
117-
}
118-
if home != "" {
119-
paths = append(paths, filepath.Join(home, ".mendix/*/modeler/mxbuild"))
171+
paths := []string{filepath.Join("/opt/mendix/*/modeler", binaryName)}
172+
if home, err := os.UserHomeDir(); err == nil {
173+
paths = append(paths, filepath.Join(home, ".mendix/*/modeler", binaryName))
120174
}
121175
return paths
122176
}
123177
}
124178

179+
// mxbuildSearchPaths returns OS-specific glob patterns for MxBuild.
180+
func mxbuildSearchPaths() []string {
181+
return mendixSearchPaths(mxbuildBinaryName())
182+
}
183+
125184
// resolveJDK21 finds a JDK 21 installation.
126185
// Priority: JAVA_HOME (verify version) > macOS java_home > java in PATH (verify version) > OS-specific known locations.
127186
func resolveJDK21() (string, error) {
@@ -183,17 +242,12 @@ func jdkSearchPaths() []string {
183242
switch runtime.GOOS {
184243
case "windows":
185244
var paths []string
186-
for _, env := range []string{"PROGRAMFILES", "PROGRAMW6432"} {
187-
if dir := os.Getenv(env); dir != "" {
188-
paths = append(paths,
189-
filepath.Join(dir, "Eclipse Adoptium", "jdk-21*"),
190-
filepath.Join(dir, "Java", "jdk-21*"),
191-
filepath.Join(dir, "Microsoft", "jdk-21*"),
192-
)
193-
}
194-
}
195-
if len(paths) == 0 {
196-
paths = []string{`C:\Program Files\Eclipse Adoptium\jdk-21*`}
245+
for _, dir := range windowsProgramDirs() {
246+
paths = append(paths,
247+
filepath.Join(dir, "Eclipse Adoptium", "jdk-21*"),
248+
filepath.Join(dir, "Java", "jdk-21*"),
249+
filepath.Join(dir, "Microsoft", "jdk-21*"),
250+
)
197251
}
198252
return paths
199253
case "darwin":

0 commit comments

Comments
 (0)