From 21b7f0a0a1e92cc528be174baf552f60cc3ff612 Mon Sep 17 00:00:00 2001 From: engalar Date: Fri, 10 Apr 2026 07:47:54 +0800 Subject: [PATCH] fix: run mx update-widgets before mx check to prevent false CE0463 Pluggable widget definitions created by mxcli may have Object properties that don't match the Type PropertyTypes schema, triggering CE0463 errors during mx check. Running update-widgets first normalizes widget Objects against installed .mpk files. Added --no-update-widgets flag to docker check and docker build commands to opt out of this behavior. Closes mendixlabs/mxcli#121 --- cmd/mxcli/docker.go | 31 ++++++--- cmd/mxcli/docker/build.go | 14 ++++ cmd/mxcli/docker/check.go | 21 ++++++ cmd/mxcli/docker/check_test.go | 120 +++++++++++++++++++++++++++++++++ 4 files changed, 176 insertions(+), 10 deletions(-) create mode 100644 cmd/mxcli/docker/check_test.go diff --git a/cmd/mxcli/docker.go b/cmd/mxcli/docker.go index ac71fe97..6b99fd43 100644 --- a/cmd/mxcli/docker.go +++ b/cmd/mxcli/docker.go @@ -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 { @@ -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") @@ -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 { @@ -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)") diff --git a/cmd/mxcli/docker/build.go b/cmd/mxcli/docker/build.go index 465856a3..68b1b4c1 100644 --- a/cmd/mxcli/docker/build.go +++ b/cmd/mxcli/docker/build.go @@ -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 } @@ -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 diff --git a/cmd/mxcli/docker/check.go b/cmd/mxcli/docker/check.go index 67630887..eb9490ad 100644 --- a/cmd/mxcli/docker/check.go +++ b/cmd/mxcli/docker/check.go @@ -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 @@ -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) diff --git a/cmd/mxcli/docker/check_test.go b/cmd/mxcli/docker/check_test.go new file mode 100644 index 00000000..ae7f6872 --- /dev/null +++ b/cmd/mxcli/docker/check_test.go @@ -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") + } +}