Skip to content

Commit fb4d8fa

Browse files
authored
Merge pull request #195 from engalar/fix/docker-run-improvements
fix: prefer Studio Pro over CDN downloads on Windows
2 parents dc3e404 + 36378fa commit fb4d8fa

File tree

8 files changed

+366
-55
lines changed

8 files changed

+366
-55
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: 45 additions & 24 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
@@ -641,9 +653,18 @@ func ensurePADFiles(productVersion string, w io.Writer) error {
641653
return nil
642654
}
643655

644-
// Check that the runtime PAD source exists
656+
// Check that the runtime PAD source exists; if not, the cached runtime is stale
657+
// (downloaded before PAD files were included). Invalidate and re-download.
645658
if _, err := os.Stat(runtimePAD); err != nil {
646-
return fmt.Errorf("runtime PAD files not found at %s", runtimePAD)
659+
fmt.Fprintf(w, " PAD files missing from cached runtime, re-downloading...\n")
660+
os.RemoveAll(runtimeDir)
661+
if _, err := DownloadRuntime(productVersion, w); err != nil {
662+
return fmt.Errorf("re-downloading runtime for PAD files: %w", err)
663+
}
664+
// Check again — some versions simply don't ship PAD files.
665+
if _, err := os.Stat(runtimePAD); err != nil {
666+
return fmt.Errorf("runtime PAD files not found at %s (not included in this Mendix version)", runtimePAD)
667+
}
647668
}
648669

649670
// Create parent directory if needed

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: 96 additions & 25 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,33 +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":
97-
return []string{
98-
`C:\Program Files\Mendix\*\modeler\mxbuild.exe`,
99-
`C:\Program Files (x86)\Mendix\*\modeler\mxbuild.exe`,
163+
var paths []string
164+
for _, dir := range windowsProgramDirs() {
165+
paths = append(paths, filepath.Join(dir, "Mendix", "*", "modeler", binaryName))
100166
}
167+
return paths
101168
case "darwin":
102-
return []string{
103-
"/Applications/Mendix/*/modeler/mxbuild",
104-
}
169+
return []string{filepath.Join("/Applications/Mendix/*/modeler", binaryName)}
105170
default: // linux
106-
home, _ := os.UserHomeDir()
107-
paths := []string{
108-
"/opt/mendix/*/modeler/mxbuild",
109-
}
110-
if home != "" {
111-
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))
112174
}
113175
return paths
114176
}
115177
}
116178

179+
// mxbuildSearchPaths returns OS-specific glob patterns for MxBuild.
180+
func mxbuildSearchPaths() []string {
181+
return mendixSearchPaths(mxbuildBinaryName())
182+
}
183+
117184
// resolveJDK21 finds a JDK 21 installation.
118185
// Priority: JAVA_HOME (verify version) > macOS java_home > java in PATH (verify version) > OS-specific known locations.
119186
func resolveJDK21() (string, error) {
@@ -174,11 +241,15 @@ func resolveMacOSJavaHome() (string, error) {
174241
func jdkSearchPaths() []string {
175242
switch runtime.GOOS {
176243
case "windows":
177-
return []string{
178-
`C:\Program Files\Eclipse Adoptium\jdk-21*`,
179-
`C:\Program Files\Java\jdk-21*`,
180-
`C:\Program Files\Microsoft\jdk-21*`,
244+
var paths []string
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+
)
181251
}
252+
return paths
182253
case "darwin":
183254
return []string{
184255
"/Library/Java/JavaVirtualMachines/temurin-21*/Contents/Home",

0 commit comments

Comments
 (0)