diff --git a/cmd/earthly/app/run.go b/cmd/earthly/app/run.go index 50047265d9..69b973a1e6 100644 --- a/cmd/earthly/app/run.go +++ b/cmd/earthly/app/run.go @@ -188,7 +188,6 @@ func (app *EarthlyApp) handleError(ctx context.Context, err error, args []string hintErr.Hint(), hintErr.Message(), ) - app.BaseCLI.Console().HelpPrint(hintErr.Hint()) return 1 case errors.As(err, &autoSkipErr): @@ -218,7 +217,6 @@ func (app *EarthlyApp) handleError(ctx context.Context, err error, args []string helpMsg = "Are you using --platform to target a different architecture? You may have to manually install QEMU.\n" + "For more information see https://docs.earthbuild.dev/guides/multi-platform\n" - app.BaseCLI.Console().HelpPrint(helpMsg) app.BaseCLI.Logbus().Run().SetGenericFatalError( time.Now(), logstream.FailureType_FAILURE_TYPE_OTHER, @@ -236,7 +234,6 @@ func (app *EarthlyApp) handleError(ctx context.Context, err error, args []string args = stringutil.FilterElementsFromList(args, "--ci") msg := "To debug your build, you can use the --interactive (-i) flag to drop into a shell of the failing RUN step" helpMsg = fmt.Sprintf("%s: %q\n", msg, strings.Join(args, " ")) - app.BaseCLI.Console().HelpPrint(helpMsg) } // This error would have been displayed earlier from the SolverMonitor. // This SetGenericFatalError is a catch-all just in case that hasn't happened. @@ -249,14 +246,49 @@ func (app *EarthlyApp) handleError(ctx context.Context, err error, args []string return 1 case strings.Contains(err.Error(), "security.insecure is not allowed"): - helpMsg := "earth --allow-privileged (earth -P) flag is required\n" + // Extract target info from error if available + targetInfo := "" + + if ie != nil && isInterpreterError { + targetInfo = ie.TargetID + } + + // If no target info from interpreter error, try to extract from args + if targetInfo == "" && len(args) > 1 { + for _, arg := range args[1:] { + if strings.HasPrefix(arg, "+") { + targetInfo = arg + break + } + } + } + + userMsg := "This build requires privileged mode." + + if targetInfo != "" { + userMsg = "Target " + targetInfo + " requires privileged mode." + } + + // Create help message with actual target if available + flagExample := "earth -P +your-target" + + if targetInfo != "" { + flagExample = "earth -P " + targetInfo + } + + helpMsg := "To fix this, use one of the following:\n" + + " • Run with the -P flag: " + flagExample + "\n" + + " • Set environment variable: export EARTHLY_ALLOW_PRIVILEGED=true\n" + + " • Add to config: earth config global.allow_privileged true" + app.BaseCLI.Logbus().Run().SetGenericFatalError( time.Now(), logstream.FailureType_FAILURE_TYPE_NEEDS_PRIVILEGED, helpMsg, - err.Error(), + userMsg, ) - app.BaseCLI.Console().HelpPrint(helpMsg) + + app.BaseCLI.Console().VerboseWarnf("Error: %s\n", err.Error()) return 9 case strings.Contains(err.Error(), errutil.EarthlyGitStdErrMagicString): @@ -277,8 +309,6 @@ func (app *EarthlyApp) handleError(ctx context.Context, err error, args []string app.BaseCLI.Console().VerboseWarnf("Error: %v\n", err.Error()) } - app.BaseCLI.Console().HelpPrint(helpMsg) - return 1 case strings.Contains(err.Error(), "failed to compute cache key") && strings.Contains(err.Error(), ": not found"): matches := notFoundRegex.FindStringSubmatch(err.Error()) @@ -310,7 +340,6 @@ func (app *EarthlyApp) handleError(ctx context.Context, err error, args []string helpMsg := fmt.Sprintf("%s responded with a rate limit error. This is usually because you are not logged in.\n"+ "You can login using the command:\n"+ " docker login%s", registryName, registryHost) - app.BaseCLI.Console().HelpPrint(helpMsg) app.BaseCLI.Logbus().Run().SetGenericFatalError( time.Now(), logstream.FailureType_FAILURE_TYPE_RATE_LIMITED, @@ -338,7 +367,6 @@ func (app *EarthlyApp) handleError(ctx context.Context, err error, args []string "Verify your account to lift this restriction." app.BaseCLI.Logbus().Run(). SetGenericFatalError(time.Now(), logstream.FailureType_FAILURE_TYPE_OTHER, helpMsg, grpcErr.Message()) - app.BaseCLI.Console().HelpPrint(helpMsg) return 1 case grpcErrOK && grpcErr.Code() != codes.Canceled: diff --git a/cmd/earthly/app/run_test.go b/cmd/earthly/app/run_test.go index 96a5337b8f..b45a92b504 100644 --- a/cmd/earthly/app/run_test.go +++ b/cmd/earthly/app/run_test.go @@ -1,6 +1,7 @@ package app import ( + "strings" "testing" "github.com/stretchr/testify/require" @@ -43,3 +44,70 @@ func TestRedactSecretsFromArgs(t *testing.T) { require.ElementsMatch(t, testCase.expected, actual) } } + +func TestExtractTargetFromArgs(t *testing.T) { + t.Parallel() + + for _, testCase := range []struct { + name string + expected string + args []string + }{ + { + name: "single target", + args: []string{"earthly", "+target"}, + expected: "+target", + }, + { + name: "target with flags before", + args: []string{"earthly", "--verbose", "+build"}, + expected: "+build", + }, + { + name: "target with flags after", + args: []string{"earthly", "+test", "--ci"}, + expected: "+test", + }, + { + name: "multiple targets - returns first", + args: []string{"earthly", "+first", "+second"}, + expected: "+first", + }, + { + name: "no target", + args: []string{"earthly", "--version"}, + expected: "", + }, + { + name: "empty args", + args: []string{}, + expected: "", + }, + { + name: "only command", + args: []string{"earthly"}, + expected: "", + }, + { + name: "target-like string but not target", + args: []string{"earthly", "not-a-target", "+actual-target"}, + expected: "+actual-target", + }, + } { + t.Run(testCase.name, func(t *testing.T) { + t.Parallel() + // Simulate the target extraction logic from handleError + var targetInfo string + if len(testCase.args) > 1 { + for _, arg := range testCase.args[1:] { + if strings.HasPrefix(arg, "+") { + targetInfo = arg + break + } + } + } + + require.Equal(t, testCase.expected, targetInfo) + }) + } +} diff --git a/logbus/formatter/formatter.go b/logbus/formatter/formatter.go index f8c2ca52ad..41f583a4a1 100644 --- a/logbus/formatter/formatter.go +++ b/logbus/formatter/formatter.go @@ -540,6 +540,10 @@ func (f *Formatter) printBuildFailure() { c.Printf("%s%s\n", msgPrefix, failure.GetErrorMessage()) + if failure.GetHelpMessage() != "" { + c.HelpPrint(failure.GetHelpMessage()) + } + f.lastOutputWasOngoingUpdate = false f.lastOutputWasProgress = false f.lastCommandOutput = nil diff --git a/tests/Earthfile b/tests/Earthfile index 89b8190f89..22debd0db3 100644 --- a/tests/Earthfile +++ b/tests/Earthfile @@ -71,6 +71,7 @@ ga-no-qemu-group4: BUILD +push-arg-test BUILD +ci-arg-test BUILD +gen-dockerfile-test + BUILD +all-privileged-tests BUILD +chown-test BUILD +env-test BUILD +env-home-test @@ -595,6 +596,68 @@ reject-privileged-import-test: DO +RUN_EARTHLY --earthfile=reject-privileged-import.earth --should_fail=true --extra_args="--allow-privileged" --target=+test-reject-copy DO +RUN_EARTHLY --earthfile=reject-privileged-import.earth --should_fail=true --extra_args="--allow-privileged" --target=+test-reject-cmd +all-privileged-tests: + BUILD +privileged-run-test + BUILD +privileged-with-docker-test + BUILD +privileged-if-test + BUILD +privileged-for-test + +privileged-run-test: + # Test that RUN --privileged without -P flag shows improved error message + RUN echo "VERSION 0.8 +test: + FROM alpine:latest + RUN --privileged echo 'this requires privileged mode' +" > Earthfile + DO +RUN_EARTHLY \ + --should_fail=true \ + --target=+test \ + --output_contains="To fix this, use one of the following:" + +privileged-with-docker-test: + # Test that WITH DOCKER without -P flag shows improved error message + RUN echo "VERSION 0.8 +test: + FROM earthbuild/dind:alpine-3.22-docker-28.3.3-r4 + WITH DOCKER + RUN docker ps + END +" > Earthfile + DO +RUN_EARTHLY \ + --should_fail=true \ + --target=+test \ + --output_contains="To fix this, use one of the following:" + +privileged-if-test: + # Test that IF --privileged without -P flag shows improved error message + RUN echo "VERSION 0.8 +test: + FROM alpine:latest + IF --privileged [ -f /proc/self/status ] + RUN echo 'found status' + END +" > Earthfile + DO +RUN_EARTHLY \ + --should_fail=true \ + --target=+test \ + --output_contains="To fix this, use one of the following:" + +privileged-for-test: + # Test that FOR --privileged without -P flag shows improved error message + RUN echo "VERSION 0.8 +test: + FROM alpine:latest + FOR --privileged file IN \$(ls) + RUN echo \$file + END +" > Earthfile + DO +RUN_EARTHLY \ + --should_fail=true \ + --target=+test \ + --output_contains="To fix this, use one of the following:" + + + pass-args-test: COPY pass-args-sub-dir.earth subdir/Earthfile COPY pass-args-root.earth Earthfile diff --git a/tests/with-docker/Earthfile b/tests/with-docker/Earthfile index 31f96fa52c..1a85a48e5a 100644 --- a/tests/with-docker/Earthfile +++ b/tests/with-docker/Earthfile @@ -178,4 +178,4 @@ pre-script-test: COPY pre-script.sh /usr/share/earthly/dockerd-wrapper-pre-script WITH DOCKER RUN test -f /the-prescript-was-run - END + END \ No newline at end of file