Skip to content

Commit fe6e61f

Browse files
authored
Merge pull request #164 from engalar/fix/ce0463-auto-update-widgets
fix: run mx update-widgets before mx check to prevent false CE0463
2 parents 47fb0a9 + 21b7f0a commit fe6e61f

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)