Skip to content

Commit 21b7f0a

Browse files
committed
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 #121
1 parent c97c85b commit 21b7f0a

4 files changed

Lines changed: 176 additions & 10 deletions

File tree

cmd/mxcli/docker.go

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -139,14 +139,16 @@ Examples:
139139
outputDir, _ := cmd.Flags().GetString("output")
140140
dryRun, _ := cmd.Flags().GetBool("dry-run")
141141
skipCheck, _ := cmd.Flags().GetBool("skip-check")
142+
noUpdateWidgets, _ := cmd.Flags().GetBool("no-update-widgets")
142143

143144
opts := docker.BuildOptions{
144-
ProjectPath: projectPath,
145-
MxBuildPath: mxbuildPath,
146-
OutputDir: outputDir,
147-
DryRun: dryRun,
148-
SkipCheck: skipCheck,
149-
Stdout: os.Stdout,
145+
ProjectPath: projectPath,
146+
MxBuildPath: mxbuildPath,
147+
OutputDir: outputDir,
148+
DryRun: dryRun,
149+
SkipCheck: skipCheck,
150+
SkipUpdateWidgets: noUpdateWidgets,
151+
Stdout: os.Stdout,
150152
}
151153

152154
if err := docker.Build(opts); err != nil {
@@ -165,11 +167,16 @@ This catches project errors (broken references, missing attributes, etc.)
165167
early, before the slower MxBuild step. The 'docker build' command runs
166168
this automatically unless --skip-check is used.
167169
170+
By default, 'mx update-widgets' runs before 'mx check' to normalize
171+
pluggable widget definitions and prevent false CE0463 errors. Use
172+
--no-update-widgets to skip this step.
173+
168174
The mx binary is located from the same directory as mxbuild.
169175
170176
Examples:
171177
mxcli docker check -p app.mpr
172178
mxcli docker check -p app.mpr --mxbuild-path /path/to/mendix
179+
mxcli docker check -p app.mpr --no-update-widgets
173180
`,
174181
Run: func(cmd *cobra.Command, args []string) {
175182
projectPath, _ := cmd.Flags().GetString("project")
@@ -179,12 +186,14 @@ Examples:
179186
}
180187

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

183191
opts := docker.CheckOptions{
184-
ProjectPath: projectPath,
185-
MxBuildPath: mxbuildPath,
186-
Stdout: os.Stdout,
187-
Stderr: os.Stderr,
192+
ProjectPath: projectPath,
193+
MxBuildPath: mxbuildPath,
194+
SkipUpdateWidgets: noUpdateWidgets,
195+
Stdout: os.Stdout,
196+
Stderr: os.Stderr,
188197
}
189198

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

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

494505
// Init command flags
495506
dockerInitCmd.Flags().StringP("output", "o", "", "Output directory (default: .docker/ next to MPR)")

cmd/mxcli/docker/build.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,9 @@ type BuildOptions struct {
3434
// SkipCheck skips the 'mx check' pre-build validation.
3535
SkipCheck bool
3636

37+
// SkipUpdateWidgets skips the 'mx update-widgets' step before checking.
38+
SkipUpdateWidgets bool
39+
3740
// Stdout for output messages.
3841
Stdout io.Writer
3942
}
@@ -94,6 +97,17 @@ func Build(opts BuildOptions) error {
9497
if err != nil {
9598
fmt.Fprintf(w, " Skipping check: %v\n", err)
9699
} else {
100+
// Run update-widgets before check to prevent false CE0463 errors
101+
if !opts.SkipUpdateWidgets {
102+
fmt.Fprintln(w, " Updating widget definitions...")
103+
uwCmd := exec.Command(mxPath, "update-widgets", opts.ProjectPath)
104+
uwCmd.Stdout = w
105+
uwCmd.Stderr = os.Stderr
106+
if err := uwCmd.Run(); err != nil {
107+
fmt.Fprintf(w, " Warning: update-widgets failed (continuing): %v\n", err)
108+
}
109+
}
110+
97111
cmd := exec.Command(mxPath, "check", opts.ProjectPath)
98112
cmd.Stdout = w
99113
cmd.Stderr = os.Stderr

cmd/mxcli/docker/check.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,11 @@ type CheckOptions struct {
2020
// MxBuildPath is an explicit path to the mxbuild executable (used to find mx).
2121
MxBuildPath string
2222

23+
// SkipUpdateWidgets skips the 'mx update-widgets' step before checking.
24+
// By default, update-widgets runs first to normalize pluggable widget
25+
// definitions and prevent false CE0463 errors.
26+
SkipUpdateWidgets bool
27+
2328
// Stdout for output messages.
2429
Stdout io.Writer
2530

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

53+
// Run mx update-widgets to normalize pluggable widget definitions.
54+
// This prevents false CE0463 ("widget definition changed") errors caused
55+
// by mismatch between widget Object properties and Type PropertyTypes.
56+
if !opts.SkipUpdateWidgets {
57+
fmt.Fprintf(w, "Updating widget definitions in %s...\n", opts.ProjectPath)
58+
uwCmd := exec.Command(mxPath, "update-widgets", opts.ProjectPath)
59+
uwCmd.Stdout = w
60+
uwCmd.Stderr = stderr
61+
if err := uwCmd.Run(); err != nil {
62+
// Non-fatal: warn and continue with check
63+
fmt.Fprintf(w, "Warning: update-widgets failed (continuing with check): %v\n", err)
64+
} else {
65+
fmt.Fprintln(w, "Widget definitions updated.")
66+
}
67+
}
68+
4869
// Run mx check
4970
fmt.Fprintf(w, "Checking project %s...\n", opts.ProjectPath)
5071
cmd := exec.Command(mxPath, "check", opts.ProjectPath)

cmd/mxcli/docker/check_test.go

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
3+
package docker
4+
5+
import (
6+
"bytes"
7+
"os"
8+
"path/filepath"
9+
"runtime"
10+
"testing"
11+
)
12+
13+
func TestCheck_SkipUpdateWidgets(t *testing.T) {
14+
// This test verifies the SkipUpdateWidgets option is wired through.
15+
// Since we don't have a real mx binary in CI, we just verify the
16+
// function returns the expected "mx not found" error.
17+
opts := CheckOptions{
18+
ProjectPath: "/nonexistent/app.mpr",
19+
SkipUpdateWidgets: true,
20+
Stdout: &bytes.Buffer{},
21+
Stderr: &bytes.Buffer{},
22+
}
23+
24+
err := Check(opts)
25+
if err == nil {
26+
t.Fatal("expected error when mx binary not found")
27+
}
28+
if got := err.Error(); got != "mx not found; specify --mxbuild-path pointing to Mendix installation directory" {
29+
// Accept any error about mx not being found
30+
t.Logf("got error: %s", got)
31+
}
32+
}
33+
34+
// createFakeMxDir creates a temp directory with fake mx and mxbuild scripts
35+
// that log their first argument to a file.
36+
func createFakeMxDir(t *testing.T) (dir, logFile string) {
37+
t.Helper()
38+
dir = t.TempDir()
39+
logFile = filepath.Join(dir, "commands.log")
40+
41+
script := `#!/bin/sh
42+
echo "$1" >> ` + logFile + "\n"
43+
44+
for _, name := range []string{"mx", "mxbuild"} {
45+
path := filepath.Join(dir, name)
46+
if err := os.WriteFile(path, []byte(script), 0755); err != nil {
47+
t.Fatal(err)
48+
}
49+
}
50+
return dir, logFile
51+
}
52+
53+
func TestCheck_UpdateWidgetsBeforeCheck(t *testing.T) {
54+
if runtime.GOOS == "windows" {
55+
t.Skip("shell script test not supported on Windows")
56+
}
57+
58+
mxDir, logFile := createFakeMxDir(t)
59+
60+
var stdout, stderr bytes.Buffer
61+
opts := CheckOptions{
62+
ProjectPath: "/tmp/fake.mpr",
63+
MxBuildPath: mxDir,
64+
Stdout: &stdout,
65+
Stderr: &stderr,
66+
}
67+
68+
Check(opts)
69+
70+
logBytes, err := os.ReadFile(logFile)
71+
if err != nil {
72+
t.Fatalf("failed to read command log: %v", err)
73+
}
74+
75+
log := string(logBytes)
76+
if !bytes.Contains(logBytes, []byte("update-widgets\n")) {
77+
t.Errorf("update-widgets was not called, got log:\n%s", log)
78+
}
79+
if !bytes.Contains(logBytes, []byte("check\n")) {
80+
t.Errorf("check was not called, got log:\n%s", log)
81+
}
82+
83+
// Verify order: update-widgets before check
84+
uwIdx := bytes.Index(logBytes, []byte("update-widgets"))
85+
chIdx := bytes.Index(logBytes, []byte("check"))
86+
if uwIdx >= chIdx {
87+
t.Errorf("update-widgets should run before check, got log:\n%s", log)
88+
}
89+
}
90+
91+
func TestCheck_SkipUpdateWidgetsFlag(t *testing.T) {
92+
if runtime.GOOS == "windows" {
93+
t.Skip("shell script test not supported on Windows")
94+
}
95+
96+
mxDir, logFile := createFakeMxDir(t)
97+
98+
var stdout, stderr bytes.Buffer
99+
opts := CheckOptions{
100+
ProjectPath: "/tmp/fake.mpr",
101+
MxBuildPath: mxDir,
102+
SkipUpdateWidgets: true,
103+
Stdout: &stdout,
104+
Stderr: &stderr,
105+
}
106+
107+
Check(opts)
108+
109+
logBytes, err := os.ReadFile(logFile)
110+
if err != nil {
111+
t.Fatalf("failed to read command log: %v", err)
112+
}
113+
114+
if bytes.Contains(logBytes, []byte("update-widgets")) {
115+
t.Error("update-widgets should NOT be called when SkipUpdateWidgets=true")
116+
}
117+
if !bytes.Contains(logBytes, []byte("check")) {
118+
t.Error("check should still be called")
119+
}
120+
}

0 commit comments

Comments
 (0)