Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 15 additions & 6 deletions cmd/mxcli/cmd_new.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,12 +57,21 @@ Examples:
os.Exit(1)
}

// Step 1: Download MxBuild
fmt.Printf("Step 1/4: Downloading MxBuild %s...\n", mendixVersion)
mxbuildPath, err := docker.DownloadMxBuild(mendixVersion, os.Stdout)
if err != nil {
fmt.Fprintf(os.Stderr, "Error downloading MxBuild: %v\n", err)
os.Exit(1)
// Step 1: Resolve MxBuild and mx binary.
// On Windows, prefer Studio Pro installation (ships both mxbuild.exe and mx.exe).
// Fall back to CDN download on other platforms or when Studio Pro is not installed.
fmt.Printf("Step 1/4: Resolving MxBuild %s...\n", mendixVersion)
var mxbuildPath string
if studioDir := docker.ResolveStudioProDir(mendixVersion); studioDir != "" {
mxbuildPath = filepath.Join(studioDir, "modeler", "mxbuild.exe")
fmt.Printf(" Using Studio Pro: %s\n", studioDir)
} else {
var err error
mxbuildPath, err = docker.DownloadMxBuild(mendixVersion, os.Stdout)
if err != nil {
fmt.Fprintf(os.Stderr, "Error downloading MxBuild: %v\n", err)
os.Exit(1)
}
}

// Resolve mx binary from mxbuild path
Expand Down
69 changes: 45 additions & 24 deletions cmd/mxcli/docker/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,9 +76,12 @@ func Build(opts BuildOptions) error {
}
fmt.Fprintf(w, " MxBuild: %s\n", mxbuildPath)

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

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

// Step 2: Ensure MxBuild is available
fmt.Fprintln(w, "Ensuring MxBuild is available...")
_, err = DownloadMxBuild(pv.ProductVersion, w)
if err != nil {
return fmt.Errorf("setting up mxbuild: %w", err)
}
// Step 2 & 3: Ensure MxBuild and runtime are available.
// On Windows, prefer Studio Pro installation directory — it ships both mxbuild.exe
// and runtime/, so no CDN download is needed.
studioProDir := ResolveStudioProDir(pv.ProductVersion)
if studioProDir != "" {
fmt.Fprintf(w, "Using Studio Pro installation: %s\n", studioProDir)
// Point MxBuildPath so Build() uses Studio Pro's mxbuild.exe
if opts.MxBuildPath == "" {
opts.MxBuildPath = studioProDir
}
} else {
fmt.Fprintln(w, "Ensuring MxBuild is available...")
_, err = DownloadMxBuild(pv.ProductVersion, w)
if err != nil {
return fmt.Errorf("setting up mxbuild: %w", err)
}

// Step 3: Ensure runtime is available
fmt.Fprintln(w, "Ensuring Mendix runtime is available...")
_, err = DownloadRuntime(pv.ProductVersion, w)
if err != nil {
return fmt.Errorf("setting up runtime: %w", err)
}
fmt.Fprintln(w, "Ensuring Mendix runtime is available...")
_, err = DownloadRuntime(pv.ProductVersion, w)
if err != nil {
return fmt.Errorf("setting up runtime: %w", err)
}

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

// Step 3c: Ensure demo users exist
Expand Down Expand Up @@ -641,9 +653,18 @@ func ensurePADFiles(productVersion string, w io.Writer) error {
return nil
}

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

// Create parent directory if needed
Expand Down
8 changes: 8 additions & 0 deletions cmd/mxcli/docker/check.go
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,14 @@ func ResolveMx(mxbuildPath string) (string, error) {
return p, nil
}

// Try OS-specific known locations (Studio Pro on Windows) before cached downloads.
for _, pattern := range mendixSearchPaths(mxBinaryName()) {
matches, _ := filepath.Glob(pattern)
if len(matches) > 0 {
return matches[len(matches)-1], nil
}
}

// Try cached mxbuild installations (~/.mxcli/mxbuild/*/modeler/mx).
// NOTE: lexicographic sort is imperfect for versions (e.g. "9.x" > "10.x"),
// but this is a fallback-of-last-resort — in practice users typically have
Expand Down
121 changes: 96 additions & 25 deletions cmd/mxcli/docker/detect.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,25 @@
// SPDX-License-Identifier: Apache-2.0

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

import (
Expand Down Expand Up @@ -49,7 +68,9 @@ func findMxBuildInDir(dir string) string {
}

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

// Try cached downloads (~/.mxcli/mxbuild/*/modeler/mxbuild)
if p := AnyCachedMxBuildPath(); p != "" {
return p, nil
}

// Try OS-specific known locations
// Try OS-specific known locations (Studio Pro on Windows) BEFORE cached downloads.
// On Windows, CDN downloads are Linux binaries — Studio Pro's mxbuild.exe is preferred.
for _, pattern := range mxbuildSearchPaths() {
matches, _ := filepath.Glob(pattern)
if len(matches) > 0 {
Expand All @@ -87,33 +104,83 @@ func resolveMxBuild(explicitPath string) (string, error) {
}
}

// Try cached downloads (~/.mxcli/mxbuild/*/modeler/mxbuild)
if p := AnyCachedMxBuildPath(); p != "" {
return p, nil
}

return "", fmt.Errorf("mxbuild not found; install Mendix Studio Pro or specify --mxbuild-path")
}

// mxbuildSearchPaths returns OS-specific glob patterns for MxBuild.
func mxbuildSearchPaths() []string {
// ResolveStudioProDir finds the Studio Pro installation directory for a specific
// Mendix version on Windows. Returns the installation root (e.g.,
// "D:\Program Files\Mendix\11.6.4") or empty string if not found.
// On non-Windows platforms, always returns empty string.
func ResolveStudioProDir(version string) string {
if runtime.GOOS != "windows" {
return ""
}
for _, dir := range windowsProgramDirs() {
candidate := filepath.Join(dir, "Mendix", version)
if info, err := os.Stat(filepath.Join(candidate, "modeler", "mxbuild.exe")); err == nil && !info.IsDir() {
return candidate
}
}
return ""
}

// windowsProgramDirs returns candidate Program Files directories on Windows,
// derived from environment variables and the system drive letter.
func windowsProgramDirs() []string {
seen := map[string]bool{}
var dirs []string
add := func(d string) {
if d != "" && !seen[d] {
seen[d] = true
dirs = append(dirs, d)
}
}
for _, env := range []string{"PROGRAMFILES", "PROGRAMW6432", "PROGRAMFILES(X86)"} {
add(os.Getenv(env))
}
// Fallback: derive from SystemDrive (e.g., "D:\Program Files").
// SystemDrive returns "D:" without a trailing separator; filepath.Join
// treats "D:" as a relative path, producing "D:Program Files" instead of
// "D:\Program Files". Append the separator explicitly.
if sysDrive := os.Getenv("SystemDrive"); sysDrive != "" {
root := sysDrive + string(os.PathSeparator)
add(filepath.Join(root, "Program Files"))
add(filepath.Join(root, "Program Files (x86)"))
}
return dirs
}

// mendixSearchPaths returns OS-specific glob patterns for a Mendix binary
// (e.g., "mxbuild.exe", "mx.exe", "mxbuild", "mx") inside Studio Pro installations.
func mendixSearchPaths(binaryName string) []string {
switch runtime.GOOS {
case "windows":
return []string{
`C:\Program Files\Mendix\*\modeler\mxbuild.exe`,
`C:\Program Files (x86)\Mendix\*\modeler\mxbuild.exe`,
var paths []string
for _, dir := range windowsProgramDirs() {
paths = append(paths, filepath.Join(dir, "Mendix", "*", "modeler", binaryName))
}
return paths
case "darwin":
return []string{
"/Applications/Mendix/*/modeler/mxbuild",
}
return []string{filepath.Join("/Applications/Mendix/*/modeler", binaryName)}
default: // linux
home, _ := os.UserHomeDir()
paths := []string{
"/opt/mendix/*/modeler/mxbuild",
}
if home != "" {
paths = append(paths, filepath.Join(home, ".mendix/*/modeler/mxbuild"))
paths := []string{filepath.Join("/opt/mendix/*/modeler", binaryName)}
if home, err := os.UserHomeDir(); err == nil {
paths = append(paths, filepath.Join(home, ".mendix/*/modeler", binaryName))
}
return paths
}
}

// mxbuildSearchPaths returns OS-specific glob patterns for MxBuild.
func mxbuildSearchPaths() []string {
return mendixSearchPaths(mxbuildBinaryName())
}

// resolveJDK21 finds a JDK 21 installation.
// Priority: JAVA_HOME (verify version) > macOS java_home > java in PATH (verify version) > OS-specific known locations.
func resolveJDK21() (string, error) {
Expand Down Expand Up @@ -174,11 +241,15 @@ func resolveMacOSJavaHome() (string, error) {
func jdkSearchPaths() []string {
switch runtime.GOOS {
case "windows":
return []string{
`C:\Program Files\Eclipse Adoptium\jdk-21*`,
`C:\Program Files\Java\jdk-21*`,
`C:\Program Files\Microsoft\jdk-21*`,
var paths []string
for _, dir := range windowsProgramDirs() {
paths = append(paths,
filepath.Join(dir, "Eclipse Adoptium", "jdk-21*"),
filepath.Join(dir, "Java", "jdk-21*"),
filepath.Join(dir, "Microsoft", "jdk-21*"),
)
}
return paths
case "darwin":
return []string{
"/Library/Java/JavaVirtualMachines/temurin-21*/Contents/Home",
Expand Down
Loading