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
31 changes: 21 additions & 10 deletions cmd/mxcli/docker.go
Original file line number Diff line number Diff line change
Expand Up @@ -139,14 +139,16 @@ Examples:
outputDir, _ := cmd.Flags().GetString("output")
dryRun, _ := cmd.Flags().GetBool("dry-run")
skipCheck, _ := cmd.Flags().GetBool("skip-check")
noUpdateWidgets, _ := cmd.Flags().GetBool("no-update-widgets")

opts := docker.BuildOptions{
ProjectPath: projectPath,
MxBuildPath: mxbuildPath,
OutputDir: outputDir,
DryRun: dryRun,
SkipCheck: skipCheck,
Stdout: os.Stdout,
ProjectPath: projectPath,
MxBuildPath: mxbuildPath,
OutputDir: outputDir,
DryRun: dryRun,
SkipCheck: skipCheck,
SkipUpdateWidgets: noUpdateWidgets,
Stdout: os.Stdout,
}

if err := docker.Build(opts); err != nil {
Expand All @@ -165,11 +167,16 @@ This catches project errors (broken references, missing attributes, etc.)
early, before the slower MxBuild step. The 'docker build' command runs
this automatically unless --skip-check is used.

By default, 'mx update-widgets' runs before 'mx check' to normalize
pluggable widget definitions and prevent false CE0463 errors. Use
--no-update-widgets to skip this step.

The mx binary is located from the same directory as mxbuild.

Examples:
mxcli docker check -p app.mpr
mxcli docker check -p app.mpr --mxbuild-path /path/to/mendix
mxcli docker check -p app.mpr --no-update-widgets
`,
Run: func(cmd *cobra.Command, args []string) {
projectPath, _ := cmd.Flags().GetString("project")
Expand All @@ -179,12 +186,14 @@ Examples:
}

mxbuildPath, _ := cmd.Flags().GetString("mxbuild-path")
noUpdateWidgets, _ := cmd.Flags().GetBool("no-update-widgets")

opts := docker.CheckOptions{
ProjectPath: projectPath,
MxBuildPath: mxbuildPath,
Stdout: os.Stdout,
Stderr: os.Stderr,
ProjectPath: projectPath,
MxBuildPath: mxbuildPath,
SkipUpdateWidgets: noUpdateWidgets,
Stdout: os.Stdout,
Stderr: os.Stderr,
}

if err := docker.Check(opts); err != nil {
Expand Down Expand Up @@ -487,9 +496,11 @@ func init() {
dockerBuildCmd.Flags().StringP("output", "o", "", "Output directory for PAD package")
dockerBuildCmd.Flags().Bool("dry-run", false, "Detect tools and show patch plan without building")
dockerBuildCmd.Flags().Bool("skip-check", false, "Skip 'mx check' pre-build validation")
dockerBuildCmd.Flags().Bool("no-update-widgets", false, "Skip 'mx update-widgets' before check")

// Check command flags
dockerCheckCmd.Flags().String("mxbuild-path", "", "Path to MxBuild/Mendix installation (used to find mx)")
dockerCheckCmd.Flags().Bool("no-update-widgets", false, "Skip 'mx update-widgets' before check")

// Init command flags
dockerInitCmd.Flags().StringP("output", "o", "", "Output directory (default: .docker/ next to MPR)")
Expand Down
14 changes: 14 additions & 0 deletions cmd/mxcli/docker/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ type BuildOptions struct {
// SkipCheck skips the 'mx check' pre-build validation.
SkipCheck bool

// SkipUpdateWidgets skips the 'mx update-widgets' step before checking.
SkipUpdateWidgets bool

// Stdout for output messages.
Stdout io.Writer
}
Expand Down Expand Up @@ -94,6 +97,17 @@ func Build(opts BuildOptions) error {
if err != nil {
fmt.Fprintf(w, " Skipping check: %v\n", err)
} else {
// Run update-widgets before check to prevent false CE0463 errors
if !opts.SkipUpdateWidgets {
fmt.Fprintln(w, " Updating widget definitions...")
uwCmd := exec.Command(mxPath, "update-widgets", opts.ProjectPath)
uwCmd.Stdout = w
uwCmd.Stderr = os.Stderr
if err := uwCmd.Run(); err != nil {
fmt.Fprintf(w, " Warning: update-widgets failed (continuing): %v\n", err)
}
}

cmd := exec.Command(mxPath, "check", opts.ProjectPath)
cmd.Stdout = w
cmd.Stderr = os.Stderr
Expand Down
21 changes: 21 additions & 0 deletions cmd/mxcli/docker/check.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@ type CheckOptions struct {
// MxBuildPath is an explicit path to the mxbuild executable (used to find mx).
MxBuildPath string

// SkipUpdateWidgets skips the 'mx update-widgets' step before checking.
// By default, update-widgets runs first to normalize pluggable widget
// definitions and prevent false CE0463 errors.
SkipUpdateWidgets bool

// Stdout for output messages.
Stdout io.Writer

Expand All @@ -45,6 +50,22 @@ func Check(opts CheckOptions) error {
}
fmt.Fprintf(w, "Using mx: %s\n", mxPath)

// Run mx update-widgets to normalize pluggable widget definitions.
// This prevents false CE0463 ("widget definition changed") errors caused
// by mismatch between widget Object properties and Type PropertyTypes.
if !opts.SkipUpdateWidgets {
fmt.Fprintf(w, "Updating widget definitions in %s...\n", opts.ProjectPath)
uwCmd := exec.Command(mxPath, "update-widgets", opts.ProjectPath)
uwCmd.Stdout = w
uwCmd.Stderr = stderr
if err := uwCmd.Run(); err != nil {
// Non-fatal: warn and continue with check
fmt.Fprintf(w, "Warning: update-widgets failed (continuing with check): %v\n", err)
} else {
fmt.Fprintln(w, "Widget definitions updated.")
}
}

// Run mx check
fmt.Fprintf(w, "Checking project %s...\n", opts.ProjectPath)
cmd := exec.Command(mxPath, "check", opts.ProjectPath)
Expand Down
120 changes: 120 additions & 0 deletions cmd/mxcli/docker/check_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
// SPDX-License-Identifier: Apache-2.0

package docker

import (
"bytes"
"os"
"path/filepath"
"runtime"
"testing"
)

func TestCheck_SkipUpdateWidgets(t *testing.T) {
// This test verifies the SkipUpdateWidgets option is wired through.
// Since we don't have a real mx binary in CI, we just verify the
// function returns the expected "mx not found" error.
opts := CheckOptions{
ProjectPath: "/nonexistent/app.mpr",
SkipUpdateWidgets: true,
Stdout: &bytes.Buffer{},
Stderr: &bytes.Buffer{},
}

err := Check(opts)
if err == nil {
t.Fatal("expected error when mx binary not found")
}
if got := err.Error(); got != "mx not found; specify --mxbuild-path pointing to Mendix installation directory" {
// Accept any error about mx not being found
t.Logf("got error: %s", got)
}
}

// createFakeMxDir creates a temp directory with fake mx and mxbuild scripts
// that log their first argument to a file.
func createFakeMxDir(t *testing.T) (dir, logFile string) {
t.Helper()
dir = t.TempDir()
logFile = filepath.Join(dir, "commands.log")

script := `#!/bin/sh
echo "$1" >> ` + logFile + "\n"

for _, name := range []string{"mx", "mxbuild"} {
path := filepath.Join(dir, name)
if err := os.WriteFile(path, []byte(script), 0755); err != nil {
t.Fatal(err)
}
}
return dir, logFile
}

func TestCheck_UpdateWidgetsBeforeCheck(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("shell script test not supported on Windows")
}

mxDir, logFile := createFakeMxDir(t)

var stdout, stderr bytes.Buffer
opts := CheckOptions{
ProjectPath: "/tmp/fake.mpr",
MxBuildPath: mxDir,
Stdout: &stdout,
Stderr: &stderr,
}

Check(opts)

logBytes, err := os.ReadFile(logFile)
if err != nil {
t.Fatalf("failed to read command log: %v", err)
}

log := string(logBytes)
if !bytes.Contains(logBytes, []byte("update-widgets\n")) {
t.Errorf("update-widgets was not called, got log:\n%s", log)
}
if !bytes.Contains(logBytes, []byte("check\n")) {
t.Errorf("check was not called, got log:\n%s", log)
}

// Verify order: update-widgets before check
uwIdx := bytes.Index(logBytes, []byte("update-widgets"))
chIdx := bytes.Index(logBytes, []byte("check"))
if uwIdx >= chIdx {
t.Errorf("update-widgets should run before check, got log:\n%s", log)
}
}

func TestCheck_SkipUpdateWidgetsFlag(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("shell script test not supported on Windows")
}

mxDir, logFile := createFakeMxDir(t)

var stdout, stderr bytes.Buffer
opts := CheckOptions{
ProjectPath: "/tmp/fake.mpr",
MxBuildPath: mxDir,
SkipUpdateWidgets: true,
Stdout: &stdout,
Stderr: &stderr,
}

Check(opts)

logBytes, err := os.ReadFile(logFile)
if err != nil {
t.Fatalf("failed to read command log: %v", err)
}

if bytes.Contains(logBytes, []byte("update-widgets")) {
t.Error("update-widgets should NOT be called when SkipUpdateWidgets=true")
}
if !bytes.Contains(logBytes, []byte("check")) {
t.Error("check should still be called")
}
}
Loading