From 7567bc0a81f9e2f1bc441647ae59415a01e61389 Mon Sep 17 00:00:00 2001 From: Luca Corbo Date: Sat, 2 Apr 2022 20:55:43 +0200 Subject: [PATCH] Add engine flag and regression fixes introduced in 1.2.0 (#99) This commit fixes some regression introduced with 1.2.0 related to tags and env variable escaping. Additionally introduces the `--engine` flags that allow to specify the container engine to use between docker and podman. The default behavior is not changed, if the flag is not specified fyne-cross will auto detect the engine. Fixes: #96, #97 --- CHANGELOG.md | 15 ++- internal/command/android_test.go | 9 ++ internal/command/command.go | 4 +- internal/command/context.go | 30 ++++-- internal/command/context_test.go | 35 ++++++- internal/command/darwin.go | 12 ++- internal/command/darwin_image.go | 20 ++-- internal/command/docker.go | 161 +++++++------------------------ internal/command/docker_test.go | 90 ++++++++++------- internal/command/engine.go | 77 +++++++++++++++ internal/command/flag.go | 22 +++++ internal/command/freebsd.go | 13 ++- internal/command/linux.go | 15 ++- internal/command/windows.go | 8 +- internal/command/windows_test.go | 17 +++- main.go | 8 +- 16 files changed, 334 insertions(+), 202 deletions(-) create mode 100644 internal/command/engine.go diff --git a/CHANGELOG.md b/CHANGELOG.md index fdaa90ca..2d796165 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,19 @@ # Changelog - Fyne.io fyne-cross -## 1.2.0 - Unreleased +## Unreleased + +### Added + +- Added the `--engine` flags that allows to specify the container engine to use + between docker and podman. The default behavior is not changed, if the flag is + not specified fyne-cross will auto detect the engine. + +### Fixed + +- Windows builds no longer pass "-H windowsgui" #97 +- Multiple tags cannot be specified using the `-tags` flag #96 + +## 1.2.0 - 07 Mar 2022 ### Added diff --git a/internal/command/android_test.go b/internal/command/android_test.go index e7288493..9fdaef66 100644 --- a/internal/command/android_test.go +++ b/internal/command/android_test.go @@ -11,6 +11,11 @@ func Test_makeAndroidContext(t *testing.T) { vol, err := mockDefaultVolume() require.Nil(t, err) + engine, err := MakeEngine(autodetectEngine) + if err != nil { + t.Skip("engine not found", err) + } + type args struct { flags *androidFlags args []string @@ -76,6 +81,8 @@ func Test_makeAndroidContext(t *testing.T) { StripDebug: true, Package: ".", Keystore: "/app/testdata/my.keystore", + Engine: engine, + Env: map[string]string{}, }, }, wantErr: false, @@ -105,6 +112,8 @@ func Test_makeAndroidContext(t *testing.T) { StripDebug: true, Package: ".", Keystore: "/app/testdata/my.keystore", + Engine: engine, + Env: map[string]string{}, }, }, wantErr: false, diff --git a/internal/command/command.go b/internal/command/command.go index 7f426111..8f5b8b3b 100644 --- a/internal/command/command.go +++ b/internal/command/command.go @@ -150,7 +150,7 @@ func fynePackageHost(ctx Context) error { // add tags to command, if any tags := ctx.Tags if len(tags) > 0 { - args = append(args, "-tags", fmt.Sprintf("'%s'", strings.Join(tags, ","))) + args = append(args, "-tags", fmt.Sprintf("%q", strings.Join(tags, ","))) } // run the command from the host @@ -196,7 +196,7 @@ func fyneReleaseHost(ctx Context) error { // add tags to command, if any tags := ctx.Tags if len(tags) > 0 { - args = append(args, "-tags", fmt.Sprintf("'%s'", strings.Join(tags, ","))) + args = append(args, "-tags", fmt.Sprintf("%q", strings.Join(tags, ","))) } switch ctx.OS { diff --git a/internal/command/context.go b/internal/command/context.go index e77f0a2d..a4230d99 100644 --- a/internal/command/context.go +++ b/internal/command/context.go @@ -39,12 +39,13 @@ type Context struct { // Volume holds the mounted volumes between host and containers volume.Volume - Architecture // Arch defines the target architecture - Env []string // Env is the list of custom env variable to set. Specified as "KEY=VALUE" - ID string // ID is the context ID - LdFlags []string // LdFlags defines the ldflags to use - OS string // OS defines the target OS - Tags []string // Tags defines the tags to use + Architecture // Arch defines the target architecture + Engine Engine // Engine is the container engine to use + Env map[string]string // Env is the list of custom env variable to set. Specified as "KEY=VALUE" + ID string // ID is the context ID + LdFlags []string // LdFlags defines the ldflags to use + OS string // OS defines the target OS + Tags []string // Tags defines the tags to use AppBuild string // Build number AppID string // AppID is the appID to use for distribution @@ -91,13 +92,23 @@ func makeDefaultContext(flags *CommonFlags, args []string) (Context, error) { return Context{}, err } + engine := flags.Engine.Engine + if (engine == Engine{}) { + // attempt to autodetect + engine, err = MakeEngine(autodetectEngine) + if err != nil { + return Context{}, err + } + } + // set context based on command-line flags ctx := Context{ AppID: flags.AppID, AppVersion: flags.AppVersion, CacheEnabled: !flags.NoCache, DockerImage: flags.DockerImage, - Env: flags.Env, + Engine: engine, + Env: make(map[string]string), Tags: flags.Tags, Icon: flags.Icon, Name: flags.Name, @@ -120,6 +131,11 @@ func makeDefaultContext(flags *CommonFlags, args []string) (Context, error) { return ctx, errors.New("output and app name should not be used as path") } + for _, v := range flags.Env { + parts := strings.SplitN(v, "=", 2) + ctx.Env[parts[0]] = parts[1] + } + ctx.AppBuild = strconv.Itoa(flags.AppBuild) ctx.Package, err = packageFromArgs(args, vol) diff --git a/internal/command/context_test.go b/internal/command/context_test.go index d734e872..8d856d57 100644 --- a/internal/command/context_test.go +++ b/internal/command/context_test.go @@ -14,6 +14,11 @@ func Test_makeDefaultContext(t *testing.T) { vol, err := mockDefaultVolume() require.Nil(t, err) + engine, err := MakeEngine(autodetectEngine) + if err != nil { + t.Skip("engine not found", err) + } + type args struct { flags *CommonFlags args []string @@ -38,6 +43,8 @@ func Test_makeDefaultContext(t *testing.T) { CacheEnabled: true, StripDebug: true, Package: ".", + Engine: engine, + Env: map[string]string{}, }, wantErr: false, }, @@ -56,7 +63,8 @@ func Test_makeDefaultContext(t *testing.T) { CacheEnabled: true, StripDebug: true, Package: ".", - Env: []string{"TEST=true"}, + Engine: engine, + Env: map[string]string{"TEST": "true"}, }, wantErr: false, }, @@ -75,7 +83,8 @@ func Test_makeDefaultContext(t *testing.T) { CacheEnabled: true, StripDebug: true, Package: ".", - Env: []string{"GOFLAGS=-mod=vendor"}, + Engine: engine, + Env: map[string]string{"GOFLAGS": "-mod=vendor"}, }, wantErr: false, }, @@ -95,6 +104,8 @@ func Test_makeDefaultContext(t *testing.T) { StripDebug: true, Package: ".", LdFlags: []string{"-X main.version=1.2.3"}, + Engine: engine, + Env: map[string]string{}, }, wantErr: false, }, @@ -112,6 +123,8 @@ func Test_makeDefaultContext(t *testing.T) { CacheEnabled: true, StripDebug: true, Package: ".", + Engine: engine, + Env: map[string]string{}, }, wantErr: false, }, @@ -130,6 +143,8 @@ func Test_makeDefaultContext(t *testing.T) { CacheEnabled: true, StripDebug: true, Package: ".", + Engine: engine, + Env: map[string]string{}, }, wantErr: false, }, @@ -148,6 +163,8 @@ func Test_makeDefaultContext(t *testing.T) { CacheEnabled: true, StripDebug: true, Package: "./cmd/command", + Engine: engine, + Env: map[string]string{}, }, wantErr: false, }, @@ -166,6 +183,8 @@ func Test_makeDefaultContext(t *testing.T) { CacheEnabled: true, StripDebug: true, Package: "./cmd/command", + Engine: engine, + Env: map[string]string{}, }, wantErr: false, }, @@ -195,6 +214,8 @@ func Test_makeDefaultContext(t *testing.T) { StripDebug: true, Package: ".", Tags: []string{"hints", "gles"}, + Engine: engine, + Env: map[string]string{}, }, wantErr: false, }, @@ -224,6 +245,8 @@ func Test_makeDefaultContext(t *testing.T) { StripDebug: true, Package: ".", Release: true, + Engine: engine, + Env: map[string]string{}, }, wantErr: false, }, @@ -245,6 +268,8 @@ func Test_makeDefaultContext(t *testing.T) { StripDebug: true, Package: ".", Release: true, + Engine: engine, + Env: map[string]string{}, }, wantErr: false, }, @@ -264,6 +289,8 @@ func Test_makeDefaultContext(t *testing.T) { StripDebug: true, Package: ".", Name: "./test", + Engine: engine, + Env: map[string]string{}, }, wantErr: true, }, @@ -283,6 +310,8 @@ func Test_makeDefaultContext(t *testing.T) { StripDebug: true, Package: ".", Name: "test", + Engine: engine, + Env: map[string]string{}, }, wantErr: false, }, @@ -303,6 +332,8 @@ func Test_makeDefaultContext(t *testing.T) { StripDebug: true, Package: ".", Name: "test", + Engine: engine, + Env: map[string]string{}, }, wantErr: false, }, diff --git a/internal/command/darwin.go b/internal/command/darwin.go index 9d3d8cd8..d9cb4b68 100644 --- a/internal/command/darwin.go +++ b/internal/command/darwin.go @@ -229,12 +229,18 @@ func darwinContext(flags *darwinFlags, args []string) ([]Context, error) { ctx.OS = darwinOS ctx.ID = fmt.Sprintf("%s-%s", ctx.OS, ctx.Architecture) ctx.Category = flags.Category - + ctx.Env["GOOS"] = "darwin" switch arch { case ArchAmd64: - ctx.Env = append(ctx.Env, "GOOS=darwin", "GOARCH=amd64", "CC=o64-clang", "CGO_CFLAGS=-mmacosx-version-min=10.12", "CGO_LDFLAGS=-mmacosx-version-min=10.12") + ctx.Env["GOARCH"] = "amd64" + ctx.Env["CC"] = "o64-clang" + ctx.Env["CGO_CFLAGS"] = "-mmacosx-version-min=10.12" + ctx.Env["CGO_LDFLAGS"] = "-mmacosx-version-min=10.12" case ArchArm64: - ctx.Env = append(ctx.Env, "CGO_LDFLAGS=-fuse-ld=lld", "GOOS=darwin", "GOARCH=arm64", "CC=oa64-clang", "CGO_CFLAGS=-mmacosx-version-min=11.1", "CGO_LDFLAGS=-mmacosx-version-min=11.1") + ctx.Env["GOARCH"] = "arm64" + ctx.Env["CC"] = "oa64-clang" + ctx.Env["CGO_CFLAGS"] = "-mmacosx-version-min=11.1" + ctx.Env["CGO_LDFLAGS"] = "-fuse-ld=lld -mmacosx-version-min=11.1" } // set context based on command-line flags diff --git a/internal/command/darwin_image.go b/internal/command/darwin_image.go index 42e74f71..579e71a8 100644 --- a/internal/command/darwin_image.go +++ b/internal/command/darwin_image.go @@ -17,6 +17,7 @@ import ( type DarwinImage struct { sdkPath string sdkVersion string + engineFlag } // Name returns the one word command name @@ -33,6 +34,7 @@ func (cmd *DarwinImage) Description() string { func (cmd *DarwinImage) Parse(args []string) error { flagSet.StringVar(&cmd.sdkPath, "xcode-path", "", "Path to the Command Line Tools for Xcode (i.e. /tmp/Command_Line_Tools_for_Xcode_12.5.dmg)") flagSet.StringVar(&cmd.sdkVersion, "sdk-version", "", "SDK version to use. Default to automatic detection") + flagSet.Var(&cmd.engineFlag, "engine", "The container engine to use. Supported engines: [docker, podman]. Default to autodetect.") flagSet.Usage = cmd.Usage flagSet.Parse(args) @@ -61,6 +63,16 @@ func (cmd *DarwinImage) Parse(args []string) error { // Run runs the command func (cmd *DarwinImage) Run() error { + engine := cmd.Engine + if (engine == Engine{}) { + // attempt to autodetect + var err error + engine, err = MakeEngine(autodetectEngine) + if err != nil { + return err + } + } + workDir, err := ioutil.TempDir(os.TempDir(), "fyne-cross-darwin-build") if err != nil { return fmt.Errorf("could not create temporary dir: %s", err) @@ -91,14 +103,8 @@ func (cmd *DarwinImage) Run() error { } log.Info("[i] macOS SDK: ", ver) - // detect engine binary - engineBinary, err := engine() - if err != nil { - log.Fatal(err.Error()) - } - // run the command from the host - dockerCmd := execabs.Command(engineBinary, "build", "--pull", "--build-arg", fmt.Sprintf("SDK_VERSION=%s", cmd.sdkVersion), "-t", darwinImage, ".") + dockerCmd := execabs.Command(engine.Binary, "build", "--pull", "--build-arg", fmt.Sprintf("SDK_VERSION=%s", cmd.sdkVersion), "-t", darwinImage, ".") dockerCmd.Dir = workDir dockerCmd.Stdout = os.Stdout dockerCmd.Stderr = os.Stderr diff --git a/internal/command/docker.go b/internal/command/docker.go index 8f5dcd59..a462c083 100644 --- a/internal/command/docker.go +++ b/internal/command/docker.go @@ -2,7 +2,6 @@ package command import ( "bytes" - "errors" "fmt" "os" "os/user" @@ -24,70 +23,18 @@ const ( gowindresBin = "/usr/local/bin/gowindres" ) -// CheckRequirements checks if the docker binary is in PATH -func CheckRequirements() error { - _, err := execabs.LookPath("docker") - if err == nil { - return nil - } - _, err = execabs.LookPath("podman") - if err != nil { - return fmt.Errorf("missed requirement: docker or podman binary not found in PATH") - } - return nil -} - // Options define the options for the docker run command type Options struct { - CacheEnabled bool // CacheEnabled if true enable go build cache - Env []string // Env is the list of custom env variable to set. Specified as "KEY=VALUE" - WorkDir string // WorkDir set the workdir, default to volume's workdir - Debug bool // Debug if true enable log verbosity -} - -// engine returns the default engine name, if no engine is found it returns an empty string and the error. -func engine() (string, error) { - if isEngineDocker() { - return "docker", nil - } - - if isEnginePodman() { - return "podman", nil - } - - return "", errors.New("no container engine found") -} - -// isEnginePodman returns true if the engine is "podman" -func isEnginePodman() bool { - _, err := execabs.LookPath("podman") - return err == nil - -} - -// isEngineDocker return true is the engine is "docker". If "docker" is an alias to podman, so it returns false. -func isEngineDocker() bool { - execPath, err := execabs.LookPath("docker") - if err != nil { - return false - } - // if "docker" comes from an alias (i.e. "podman-docker") should not contain the "docker" string - out, err := execabs.Command(execPath, "--version").Output() - if err != nil { - return false - } - return strings.Contains(strings.ToLower(string(out)), "docker") + CacheEnabled bool // CacheEnabled if true enable go build cache + Engine Engine // Engine is the container engine to use + Env map[string]string // Env is the list of custom env variable to set + WorkDir string // WorkDir set the workdir, default to volume's workdir + Debug bool // Debug if true enable log verbosity } // Cmd returns a command to run in a new container for the specified image func Cmd(image string, vol volume.Volume, opts Options, cmdArgs []string) *execabs.Cmd { - // detect engine binary - engineBinary, err := engine() - if err != nil { - log.Fatal(err.Error()) - } - // define workdir w := vol.WorkDirContainer() if opts.WorkDir != "" { @@ -106,7 +53,7 @@ func Cmd(image string, vol volume.Volume, opts Options, cmdArgs []string) *execa } // handle settings related to engine - if isEnginePodman() { + if opts.Engine.IsPodman() { args = append(args, "--userns", "keep-id", "-e", "use_podman=1") } else { // docker: pass current user id to handle mount permissions on linux and MacOS @@ -130,8 +77,14 @@ func Cmd(image string, vol volume.Volume, opts Options, cmdArgs []string) *execa ) // add custom env variables - for _, e := range opts.Env { - args = append(args, "-e", e) + for k, v := range opts.Env { + env := k + "=" + v + if opts.Env["GOOS"] != freebsdOS && strings.Contains(v, "=") { + // engine requires to double quote the env var when value contains + // the `=` char + env = fmt.Sprintf("%q", env) + } + args = append(args, "-e", env) } // specify the image to use @@ -140,7 +93,7 @@ func Cmd(image string, vol volume.Volume, opts Options, cmdArgs []string) *execa // add the command to execute args = append(args, cmdArgs...) - cmd := execabs.Command(engineBinary, args...) + cmd := execabs.Command(opts.Engine.Binary, args...) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr @@ -168,7 +121,7 @@ func goModInit(ctx Context) error { } log.Info("[i] go.mod not found, creating a temporary one...") - runOpts := Options{Debug: ctx.Debug} + runOpts := Options{Debug: ctx.Debug, Engine: ctx.Engine} err = Run(ctx.DockerImage, ctx.Volume, runOpts, []string{"go", "mod", "init", ctx.Name}) if err != nil { return fmt.Errorf("could not generate the temporary go module: %v", err) @@ -178,26 +131,6 @@ func goModInit(ctx Context) error { return nil } -// findGoFlags return the index of context where GOFLAGS is set, or -1 if it is not present. -func findGoFlags(ctx Context) int { - for i, env := range ctx.Env { - if strings.HasPrefix(env, "GOFLAGS") { - return i - } - } - return -1 -} - -// findCgoLdFlags return the index of context where CGO_LDFLAGS is set, or -1 if it is not present. -func findCgoLdFlags(ctx Context) int { - for i, env := range ctx.Env { - if strings.HasPrefix(env, "CGO_LDFLAGS") { - return i - } - } - return -1 -} - // goBuild run the go build command in the container func goBuild(ctx Context) error { log.Infof("[i] Building binary...") @@ -208,51 +141,29 @@ func goBuild(ctx Context) error { if ctx.StripDebug { // ensure that CGO_LDFLAGS is not overwritten as they can be passed // by the --env argument - if idx := findCgoLdFlags(ctx); idx > -1 { + if v, ok := ctx.Env["CGO_LDFLAGS"]; ok { // append the ldflags after the existing CGO_LDFLAGS - ctx.Env[idx] += " -w -s" + ctx.Env["CGO_LDFLAGS"] = strings.Trim(v, "\"") + " -w -s" } else { // create the CGO_LDFLAGS environment variable and add the strip debug flags - ctx.Env = append(ctx.Env, "CGO_LDFLAGS=-w -s") + ctx.Env["CGO_LDFLAGS"] = "-w -s" } } ldflags := ctx.LdFlags - // add ldflags to command, if any - if len(ldflags) > 0 { - flags := make([]string, len(ldflags)) - for i, flag := range ldflags { - parts := strings.Split(flag, "-") // because there could be several -X flags - for _, p := range parts { - if strings.HasPrefix(p, "X") { - // We must rebuild cases - // if the user pass "-ldflags "-X A.B=C" so we need to set it to "-X=A.B=C" - // if the user pass "-ldflags "-X=A.B=C" so we should not change it - sp := strings.SplitN(p, " ", 1) - if len(sp) == 2 { - args = append(args, "-ldflags=-X="+sp[1]) - } else { - args = append(args, "-ldflags=-"+p) - } - } else { - // others flags can be set to GOFLAGS - flags[i] = "-ldflags=-" + p - } - } - } + // honour the GOFLAGS env variable adding to existing ones + if v, ok := ctx.Env["GOFLAGS"]; ok { + ldflags = append(ldflags, v) + } - // ensure that GOFLAGS is not overwritten as they can be passed - // by the --env argument - if idx := findGoFlags(ctx); idx > -1 { - // append the ldflags after the existing GOFLAGS - ctx.Env[idx] += " " + strings.Join(flags, " ") - } + if len(ldflags) > 0 { + args = append(args, "-ldflags", strings.Join(ldflags, " ")) } // add tags to command, if any tags := ctx.Tags if len(tags) > 0 { - args = append(args, "-tags", fmt.Sprintf("'%s'", strings.Join(tags, ","))) + args = append(args, "-tags", strings.Join(tags, ",")) } // set output folder to fyne-cross/bin/ @@ -269,6 +180,7 @@ func goBuild(ctx Context) error { args = append(args, ctx.Package) runOpts := Options{ CacheEnabled: ctx.CacheEnabled, + Engine: ctx.Engine, Env: ctx.Env, Debug: ctx.Debug, } @@ -287,7 +199,7 @@ func goBuild(ctx Context) error { func fynePackage(ctx Context) error { if ctx.Debug { - err := Run(ctx.DockerImage, ctx.Volume, Options{Debug: ctx.Debug}, []string{fyneBin, "version"}) + err := Run(ctx.DockerImage, ctx.Volume, Options{Debug: ctx.Debug, Engine: ctx.Engine}, []string{fyneBin, "version"}) if err != nil { return fmt.Errorf("could not get fyne cli %s version: %v", fyneBin, err) } @@ -315,7 +227,7 @@ func fynePackage(ctx Context) error { // add tags to command, if any tags := ctx.Tags if len(tags) > 0 { - args = append(args, "-tags", fmt.Sprintf("'%s'", strings.Join(tags, ","))) + args = append(args, "-tags", fmt.Sprintf("%q", strings.Join(tags, ","))) } // Enable release mode, if specified @@ -341,6 +253,7 @@ func fynePackage(ctx Context) error { CacheEnabled: ctx.CacheEnabled, WorkDir: workDir, Debug: ctx.Debug, + Engine: ctx.Engine, Env: ctx.Env, } @@ -356,7 +269,7 @@ func fynePackage(ctx Context) error { func fyneRelease(ctx Context) error { if ctx.Debug { - err := Run(ctx.DockerImage, ctx.Volume, Options{Debug: ctx.Debug}, []string{fyneBin, "version"}) + err := Run(ctx.DockerImage, ctx.Volume, Options{Debug: ctx.Debug, Engine: ctx.Engine}, []string{fyneBin, "version"}) if err != nil { return fmt.Errorf("could not get fyne cli %s version: %v", fyneBin, err) } @@ -384,7 +297,7 @@ func fyneRelease(ctx Context) error { // add tags to command, if any tags := ctx.Tags if len(tags) > 0 { - args = append(args, "-tags", fmt.Sprintf("'%s'", strings.Join(tags, ","))) + args = append(args, "-tags", fmt.Sprintf("%q", strings.Join(tags, ","))) } // workDir default value @@ -425,6 +338,7 @@ func fyneRelease(ctx Context) error { CacheEnabled: ctx.CacheEnabled, WorkDir: workDir, Debug: ctx.Debug, + Engine: ctx.Engine, Env: ctx.Env, } @@ -448,6 +362,7 @@ func WindowsResource(ctx Context) (string, error) { runOpts := Options{ Debug: ctx.Debug, + Engine: ctx.Engine, WorkDir: volume.JoinPathContainer(ctx.TmpDirContainer(), ctx.ID), } @@ -479,11 +394,7 @@ func pullImage(ctx Context) error { buf := bytes.Buffer{} // run the command inside the container - runner, err := engine() - if err != nil { - log.Fatal(err.Error()) - } - cmd := execabs.Command(runner, "pull", ctx.DockerImage) + cmd := execabs.Command(ctx.Engine.Binary, "pull", ctx.DockerImage) cmd.Stdout = &buf cmd.Stderr = &buf @@ -491,7 +402,7 @@ func pullImage(ctx Context) error { log.Debug(cmd) } - err = cmd.Run() + err := cmd.Run() if ctx.Debug { log.Debug(buf.String()) diff --git a/internal/command/docker_test.go b/internal/command/docker_test.go index 196295fc..3449882c 100644 --- a/internal/command/docker_test.go +++ b/internal/command/docker_test.go @@ -2,6 +2,7 @@ package command import ( "fmt" + "log" "os" "os/user" "path/filepath" @@ -14,17 +15,15 @@ import ( ) func TestCmdEngineDocker(t *testing.T) { - engineBinary, err := engine() + engine, err := MakeEngine(dockerEngine) if err != nil { - t.Skip("engine not found", err) + t.Skip("docker engine not found", err) } - if isEnginePodman() { - t.Skip("engine found: podman") - } - - expectedCmd, err := execabs.LookPath(engineBinary) + log.Println(engine.String()) + expectedCmd, err := execabs.LookPath(engine.String()) require.NoError(t, err) + log.Println(expectedCmd) uid, _ := user.Current() @@ -54,12 +53,14 @@ func TestCmdEngineDocker(t *testing.T) { { name: "default", args: args{ - image: "docker.io/fyneio/fyne-cross", - vol: vol, - opts: Options{}, + image: "docker.io/fyneio/fyne-cross", + vol: vol, + opts: Options{ + Engine: engine, + }, cmdArgs: []string{"command", "arg"}, }, - want: fmt.Sprintf("%s run --rm -t -w /app -v %s:/app:z -u %s:%s --entrypoint fixuid -e CGO_ENABLED=1 -e GOCACHE=/go/go-build %s -q command arg", expectedCmd, workDir, uid.Uid, uid.Uid, dockerImage), + want: fmt.Sprintf("%s run --rm -t -w /app -v %s:/app:z -u %s:%s --entrypoint fixuid -e CGO_ENABLED=1 -e GOCACHE=/go/go-build %s -q command arg", expectedCmd, workDir, uid.Uid, uid.Gid, dockerImage), wantWindows: fmt.Sprintf("%s run --rm -t -w /app -v %s:/app:z -e CGO_ENABLED=1 -e GOCACHE=/go/go-build %s command arg", expectedCmd, workDir, dockerImage), }, { @@ -68,11 +69,12 @@ func TestCmdEngineDocker(t *testing.T) { image: "docker.io/fyneio/fyne-cross", vol: vol, opts: Options{ + Engine: engine, WorkDir: customWorkDir, }, cmdArgs: []string{"command", "arg"}, }, - want: fmt.Sprintf("%s run --rm -t -w %s -v %s:/app:z -u %s:%s --entrypoint fixuid -e CGO_ENABLED=1 -e GOCACHE=/go/go-build %s -q command arg", expectedCmd, customWorkDir, workDir, uid.Uid, uid.Uid, dockerImage), + want: fmt.Sprintf("%s run --rm -t -w %s -v %s:/app:z -u %s:%s --entrypoint fixuid -e CGO_ENABLED=1 -e GOCACHE=/go/go-build %s -q command arg", expectedCmd, customWorkDir, workDir, uid.Uid, uid.Gid, dockerImage), wantWindows: fmt.Sprintf("%s run --rm -t -w %s -v %s:/app:z -e CGO_ENABLED=1 -e GOCACHE=/go/go-build %s command arg", expectedCmd, customWorkDir, workDir, dockerImage), }, { @@ -81,11 +83,12 @@ func TestCmdEngineDocker(t *testing.T) { image: "docker.io/fyneio/fyne-cross", vol: vol, opts: Options{ + Engine: engine, CacheEnabled: true, }, cmdArgs: []string{"command", "arg"}, }, - want: fmt.Sprintf("%s run --rm -t -w /app -v %s:/app:z -v %s:/go:z -u %s:%s --entrypoint fixuid -e CGO_ENABLED=1 -e GOCACHE=/go/go-build %s -q command arg", expectedCmd, workDir, cacheDir, uid.Uid, uid.Uid, dockerImage), + want: fmt.Sprintf("%s run --rm -t -w /app -v %s:/app:z -v %s:/go:z -u %s:%s --entrypoint fixuid -e CGO_ENABLED=1 -e GOCACHE=/go/go-build %s -q command arg", expectedCmd, workDir, cacheDir, uid.Uid, uid.Gid, dockerImage), wantWindows: fmt.Sprintf("%s run --rm -t -w /app -v %s:/app:z -v %s:/go:z -e CGO_ENABLED=1 -e GOCACHE=/go/go-build %s command arg", expectedCmd, workDir, cacheDir, dockerImage), }, { @@ -94,12 +97,15 @@ func TestCmdEngineDocker(t *testing.T) { image: "docker.io/fyneio/fyne-cross", vol: vol, opts: Options{ - Env: []string{"GOPROXY=proxy.example.com", "GOSUMDB=sum.example.com"}, + Engine: engine, + Env: map[string]string{ + "GOPROXY": "proxy.example.com", + }, }, cmdArgs: []string{"command", "arg"}, }, - want: fmt.Sprintf("%s run --rm -t -w /app -v %s:/app:z -u %s:%s --entrypoint fixuid -e CGO_ENABLED=1 -e GOCACHE=/go/go-build -e GOPROXY=proxy.example.com -e GOSUMDB=sum.example.com %s -q command arg", expectedCmd, workDir, uid.Uid, uid.Uid, dockerImage), - wantWindows: fmt.Sprintf("%s run --rm -t -w /app -v %s:/app:z -e CGO_ENABLED=1 -e GOCACHE=/go/go-build -e GOPROXY=proxy.example.com -e GOSUMDB=sum.example.com %s command arg", expectedCmd, workDir, dockerImage), + want: fmt.Sprintf("%s run --rm -t -w /app -v %s:/app:z -u %s:%s --entrypoint fixuid -e CGO_ENABLED=1 -e GOCACHE=/go/go-build -e GOPROXY=proxy.example.com %s -q command arg", expectedCmd, workDir, uid.Uid, uid.Gid, dockerImage), + wantWindows: fmt.Sprintf("%s run --rm -t -w /app -v %s:/app:z -e CGO_ENABLED=1 -e GOCACHE=/go/go-build -e GOPROXY=proxy.example.com %s command arg", expectedCmd, workDir, dockerImage), }, } for _, tt := range tests { @@ -110,23 +116,19 @@ func TestCmdEngineDocker(t *testing.T) { want = tt.wantWindows } if cmd != want { - t.Errorf("Cmd() command = %v, want %v", cmd, want) + t.Errorf("Cmd()\ngot :%v\nwant:%v", cmd, want) } }) } } func TestCmdEnginePodman(t *testing.T) { - engineBinary, err := engine() + engine, err := MakeEngine(podmanEngine) if err != nil { - t.Skip("engine not found", err) + t.Skip("podman engine not found", err) } - if isEngineDocker() { - t.Skip("engine found: docker") - } - - expectedCmd, err := execabs.LookPath(engineBinary) + expectedCmd, err := execabs.LookPath(engine.String()) require.NoError(t, err) workDir := filepath.Join(os.TempDir(), "fyne-cross-test", "app") @@ -155,12 +157,14 @@ func TestCmdEnginePodman(t *testing.T) { { name: "default", args: args{ - image: "docker.io/fyneio/fyne-cross", - vol: vol, - opts: Options{}, + image: "docker.io/fyneio/fyne-cross", + vol: vol, + opts: Options{ + Engine: engine, + }, cmdArgs: []string{"command", "arg"}, }, - want: fmt.Sprintf("%s run --rm -t -w /app -v %s:/app:z %s -e CGO_ENABLED=1 -e GOCACHE=/go/go-build %s -q command arg", expectedCmd, workDir, podmanFlags, dockerImage), + want: fmt.Sprintf("%s run --rm -t -w /app -v %s:/app:z %s -e CGO_ENABLED=1 -e GOCACHE=/go/go-build %s command arg", expectedCmd, workDir, podmanFlags, dockerImage), }, { name: "custom work dir", @@ -168,11 +172,12 @@ func TestCmdEnginePodman(t *testing.T) { image: "docker.io/fyneio/fyne-cross", vol: vol, opts: Options{ + Engine: engine, WorkDir: customWorkDir, }, cmdArgs: []string{"command", "arg"}, }, - want: fmt.Sprintf("%s run --rm -t -w %s -v %s:/app:z %s -e CGO_ENABLED=1 -e GOCACHE=/go/go-build %s -q command arg", expectedCmd, customWorkDir, workDir, podmanFlags, dockerImage), + want: fmt.Sprintf("%s run --rm -t -w %s -v %s:/app:z %s -e CGO_ENABLED=1 -e GOCACHE=/go/go-build %s command arg", expectedCmd, customWorkDir, workDir, podmanFlags, dockerImage), }, { name: "cache enabled", @@ -180,11 +185,12 @@ func TestCmdEnginePodman(t *testing.T) { image: "docker.io/fyneio/fyne-cross", vol: vol, opts: Options{ + Engine: engine, CacheEnabled: true, }, cmdArgs: []string{"command", "arg"}, }, - want: fmt.Sprintf("%s run --rm -t -w /app -v %s:/app:z %s -v %s:/go:z -e CGO_ENABLED=1 -e GOCACHE=/go/go-build %s -q command arg", expectedCmd, workDir, podmanFlags, cacheDir, dockerImage), + want: fmt.Sprintf("%s run --rm -t -w /app -v %s:/app:z -v %s:/go:z %s -e CGO_ENABLED=1 -e GOCACHE=/go/go-build %s command arg", expectedCmd, workDir, cacheDir, podmanFlags, dockerImage), }, { name: "custom env variables", @@ -192,11 +198,29 @@ func TestCmdEnginePodman(t *testing.T) { image: "docker.io/fyneio/fyne-cross", vol: vol, opts: Options{ - Env: []string{"GOPROXY=proxy.example.com", "GOSUMDB=sum.example.com"}, + Engine: engine, + Env: map[string]string{ + "GOPROXY": "proxy.example.com", + }, + }, + cmdArgs: []string{"command", "arg"}, + }, + want: fmt.Sprintf("%s run --rm -t -w /app -v %s:/app:z %s -e CGO_ENABLED=1 -e GOCACHE=/go/go-build -e GOPROXY=proxy.example.com %s command arg", expectedCmd, workDir, podmanFlags, dockerImage), + }, + { + name: "strip", + args: args{ + image: "docker.io/fyneio/fyne-cross", + vol: vol, + opts: Options{ + Engine: engine, + Env: map[string]string{ + "GOPROXY": "proxy.example.com", + }, }, cmdArgs: []string{"command", "arg"}, }, - want: fmt.Sprintf("%s run --rm -t -w /app -v %s:/app:z %s -e CGO_ENABLED=1 -e GOCACHE=/go/go-build -e GOPROXY=proxy.example.com -e GOSUMDB=sum.example.com %s -q command arg", expectedCmd, workDir, podmanFlags, dockerImage), + want: fmt.Sprintf("%s run --rm -t -w /app -v %s:/app:z %s -e CGO_ENABLED=1 -e GOCACHE=/go/go-build -e GOPROXY=proxy.example.com %s command arg", expectedCmd, workDir, podmanFlags, dockerImage), }, } for _, tt := range tests { @@ -204,7 +228,7 @@ func TestCmdEnginePodman(t *testing.T) { cmd := Cmd(tt.args.image, tt.args.vol, tt.args.opts, tt.args.cmdArgs).String() want := tt.want if cmd != want { - t.Errorf("Cmd() command = %v, want %v", cmd, want) + t.Errorf("Cmd()\ngot :%v\nwant:%v", cmd, want) } }) } diff --git a/internal/command/engine.go b/internal/command/engine.go new file mode 100644 index 00000000..2a66bc26 --- /dev/null +++ b/internal/command/engine.go @@ -0,0 +1,77 @@ +package command + +import ( + "errors" + "fmt" + "strings" + + "golang.org/x/sys/execabs" +) + +const ( + autodetectEngine = "" + dockerEngine = "docker" + podmanEngine = "podman" +) + +type Engine struct { + Name string + Binary string +} + +func (e Engine) String() string { + return e.Name +} + +func (e Engine) IsDocker() bool { + return e.Name == dockerEngine +} + +func (e Engine) IsPodman() bool { + return e.Name == podmanEngine +} + +// MakeEngine returns a new container engine. Pass empty string to autodetect +func MakeEngine(e string) (Engine, error) { + switch e { + case dockerEngine: + binaryPath, err := execabs.LookPath(dockerEngine) + if err != nil { + return Engine{}, fmt.Errorf("docker binary not found in PATH") + } + return Engine{Name: dockerEngine, Binary: binaryPath}, nil + case podmanEngine: + binaryPath, err := execabs.LookPath(podmanEngine) + if err != nil { + return Engine{}, fmt.Errorf("podman binary not found in PATH") + } + return Engine{Name: podmanEngine, Binary: binaryPath}, nil + case "": + binaryPath, err := execabs.LookPath(dockerEngine) + if err != nil { + // check for podman engine + binaryPath, err := execabs.LookPath(podmanEngine) + if err != nil { + return Engine{}, fmt.Errorf("engine binary not found in PATH") + } + return Engine{Name: podmanEngine, Binary: binaryPath}, nil + } + // docker binary found, check if it is an alias to podman + // if "docker" comes from an alias (i.e. "podman-docker") should not contain the "docker" string + out, err := execabs.Command(binaryPath, "--version").Output() + if err != nil { + return Engine{}, fmt.Errorf("could not detect engine version: %s", out) + } + lout := strings.ToLower(string(out)) + switch { + case strings.Contains(lout, dockerEngine): + return Engine{Name: dockerEngine, Binary: binaryPath}, nil + case strings.Contains(lout, podmanEngine): + return Engine{Name: podmanEngine, Binary: binaryPath}, nil + default: + return Engine{}, fmt.Errorf("could not detect engine version: %s", out) + } + default: + return Engine{}, errors.New("unsupported container engine") + } +} diff --git a/internal/command/flag.go b/internal/command/flag.go index 7a045c0d..dcb452af 100644 --- a/internal/command/flag.go +++ b/internal/command/flag.go @@ -28,6 +28,8 @@ type CommonFlags struct { CacheDir string // DockerImage represents a custom docker image to use for build DockerImage string + // Engine is the container engine to use + Engine engineFlag // Env is the list of custom env variable to set. Specified as "KEY=VALUE" Env envFlag // Icon represents the application icon used for distribution @@ -103,6 +105,7 @@ func newCommonFlags() (*CommonFlags, error) { flagSet.StringVar(&flags.AppVersion, "app-version", appVersion, "Version number in the form x, x.y or x.y.z semantic version") flagSet.StringVar(&flags.CacheDir, "cache", cacheDir, "Directory used to share/cache sources and dependencies") flagSet.BoolVar(&flags.NoCache, "no-cache", false, "Do not use the go build cache") + flagSet.Var(&flags.Engine, "engine", "The container engine to use. Supported engines: [docker, podman]. Default to autodetect.") flagSet.Var(&flags.Env, "env", "List of additional env variables specified as KEY=VALUE") flagSet.StringVar(&flags.Icon, "icon", defaultIcon, "Application icon used for distribution") flagSet.StringVar(&flags.DockerImage, "image", "", "Custom docker image to use for build") @@ -128,6 +131,25 @@ func defaultName() (string, error) { return output, nil } +// engineFlag is a custom flag used to define custom engine variables +type engineFlag struct { + Engine +} + +// String is the method to format the flag's value, part of the flag.Value interface. +// The String method's output will be used in diagnostics. +func (ef *engineFlag) String() string { + return fmt.Sprint(*ef) +} + +// Set is the method to set the flag value, part of the flag.Value interface. +// Set's argument is a string to be parsed to set the flag. +func (ef *engineFlag) Set(value string) error { + var err error + ef.Engine, err = MakeEngine(value) + return err +} + // envFlag is a custom flag used to define custom env variables type envFlag []string diff --git a/internal/command/freebsd.go b/internal/command/freebsd.go index becd03f2..0f57f932 100644 --- a/internal/command/freebsd.go +++ b/internal/command/freebsd.go @@ -190,15 +190,22 @@ func freebsdContext(flags *freebsdFlags, args []string) ([]Context, error) { ctx.Architecture = arch ctx.OS = freebsdOS ctx.ID = fmt.Sprintf("%s-%s", ctx.OS, ctx.Architecture) - + ctx.Env["GOOS"] = "freebsd" var defaultDockerImage string switch arch { case ArchAmd64: defaultDockerImage = freebsdImageAmd64 - ctx.Env = append(ctx.Env, "GOOS=freebsd", "GOARCH=amd64", "CC=x86_64-unknown-freebsd12-clang") + ctx.Env["GOARCH"] = "amd64" + ctx.Env["CC"] = "x86_64-unknown-freebsd12-clang" case ArchArm64: defaultDockerImage = freebsdImageArm64 - ctx.Env = append(ctx.Env, "CGO_LDFLAGS=-fuse-ld=lld", "GOOS=freebsd", "GOARCH=arm64", "CC=aarch64-unknown-freebsd12-clang") + ctx.Env["GOARCH"] = "arm64" + if v, ok := ctx.Env["CGO_LDFLAGS"]; ok { + ctx.Env["CGO_LDFLAGS"] = v + " -fuse-ld=lld" + } else { + ctx.Env["CGO_LDFLAGS"] = "-fuse-ld=lld" + } + ctx.Env["CC"] = "aarch64-unknown-freebsd12-clang" } // set context based on command-line flags diff --git a/internal/command/linux.go b/internal/command/linux.go index 375887f5..f60154ee 100644 --- a/internal/command/linux.go +++ b/internal/command/linux.go @@ -191,22 +191,27 @@ func linuxContext(flags *linuxFlags, args []string) ([]Context, error) { ctx.Architecture = arch ctx.OS = linuxOS ctx.ID = fmt.Sprintf("%s-%s", ctx.OS, ctx.Architecture) - + ctx.Env["GOOS"] = "linux" var defaultDockerImage string switch arch { case ArchAmd64: defaultDockerImage = linuxImageAmd64 - ctx.Env = append(ctx.Env, "GOOS=linux", "GOARCH=amd64", "CC=gcc") + ctx.Env["GOARCH"] = "amd64" + ctx.Env["CC"] = "gcc" case Arch386: defaultDockerImage = linuxImage386 - ctx.Env = append(ctx.Env, "GOOS=linux", "GOARCH=386", "CC=i686-linux-gnu-gcc") + ctx.Env["GOARCH"] = "386" + ctx.Env["CC"] = "i686-linux-gnu-gcc" case ArchArm: defaultDockerImage = linuxImageArm - ctx.Env = append(ctx.Env, "GOOS=linux", "GOARCH=arm", "CC=arm-linux-gnueabihf-gcc", "GOARM=7") + ctx.Env["GOARCH"] = "arm" + ctx.Env["CC"] = "arm-linux-gnueabihf-gcc" + ctx.Env["GOARM"] = "7" ctx.Tags = append(ctx.Tags, "gles") case ArchArm64: defaultDockerImage = linuxImageArm64 - ctx.Env = append(ctx.Env, "GOOS=linux", "GOARCH=arm64", "CC=aarch64-linux-gnu-gcc") + ctx.Env["GOARCH"] = "arm64" + ctx.Env["CC"] = "aarch64-linux-gnu-gcc" ctx.Tags = append(ctx.Tags, "gles") } diff --git a/internal/command/windows.go b/internal/command/windows.go index 3bc40452..bdeb9b08 100644 --- a/internal/command/windows.go +++ b/internal/command/windows.go @@ -248,12 +248,14 @@ func makeWindowsContext(flags *windowsFlags, args []string) ([]Context, error) { ctx.Certificate = flags.Certificate ctx.Developer = flags.Developer ctx.Password = flags.Password - + ctx.Env["GOOS"] = "windows" switch arch { case ArchAmd64: - ctx.Env = append(ctx.Env, "GOOS=windows", "GOARCH=amd64", "CC=x86_64-w64-mingw32-gcc") + ctx.Env["GOARCH"] = "amd64" + ctx.Env["CC"] = "x86_64-w64-mingw32-gcc" case Arch386: - ctx.Env = append(ctx.Env, "GOOS=windows", "GOARCH=386", "CC=i686-w64-mingw32-gcc") + ctx.Env["GOARCH"] = "386" + ctx.Env["CC"] = "i686-w64-mingw32-gcc" } if !flags.Console { diff --git a/internal/command/windows_test.go b/internal/command/windows_test.go index cd3caa3d..27beb536 100644 --- a/internal/command/windows_test.go +++ b/internal/command/windows_test.go @@ -11,6 +11,11 @@ func Test_makeWindowsContext(t *testing.T) { vol, err := mockDefaultVolume() require.Nil(t, err) + engine, err := MakeEngine(autodetectEngine) + if err != nil { + t.Skip("engine not found", err) + } + type args struct { flags *windowsFlags args []string @@ -41,7 +46,8 @@ func Test_makeWindowsContext(t *testing.T) { ID: "windows-amd64", OS: "windows", Architecture: "amd64", - Env: []string{"GOOS=windows", "GOARCH=amd64", "CC=x86_64-w64-mingw32-gcc"}, + Engine: engine, + Env: map[string]string{"GOOS": "windows", "GOARCH": "amd64", "CC": "x86_64-w64-mingw32-gcc"}, LdFlags: []string{"-H=windowsgui"}, DockerImage: windowsImage, }, @@ -68,7 +74,8 @@ func Test_makeWindowsContext(t *testing.T) { ID: "windows-386", OS: "windows", Architecture: "386", - Env: []string{"GOOS=windows", "GOARCH=386", "CC=i686-w64-mingw32-gcc"}, + Engine: engine, + Env: map[string]string{"GOOS": "windows", "GOARCH": "386", "CC": "i686-w64-mingw32-gcc"}, DockerImage: windowsImage, }, }, @@ -94,7 +101,8 @@ func Test_makeWindowsContext(t *testing.T) { ID: "windows-amd64", OS: "windows", Architecture: "amd64", - Env: []string{"GOOS=windows", "GOARCH=amd64", "CC=x86_64-w64-mingw32-gcc"}, + Engine: engine, + Env: map[string]string{"GOOS": "windows", "GOARCH": "amd64", "CC": "x86_64-w64-mingw32-gcc"}, LdFlags: []string{"-X main.version=1.2.3", "-H=windowsgui"}, DockerImage: windowsImage, }, @@ -121,7 +129,8 @@ func Test_makeWindowsContext(t *testing.T) { ID: "windows-amd64", OS: "windows", Architecture: "amd64", - Env: []string{"GOOS=windows", "GOARCH=amd64", "CC=x86_64-w64-mingw32-gcc"}, + Engine: engine, + Env: map[string]string{"GOOS": "windows", "GOARCH": "amd64", "CC": "x86_64-w64-mingw32-gcc"}, LdFlags: []string{"-H=windowsgui"}, DockerImage: "test", }, diff --git a/main.go b/main.go index cae8faf3..ef13e5a5 100644 --- a/main.go +++ b/main.go @@ -42,16 +42,10 @@ func main() { os.Exit(1) } - // check requirements - err := command.CheckRequirements() - if err != nil { - log.Fatalf("[✗] %s", err) - } - // Parse the arguments for the command // It will display the command usage if -help is specified // and will exit in case of error - err = cmd.Parse(os.Args[2:]) + err := cmd.Parse(os.Args[2:]) if err != nil { log.Fatalf("[✗] %s", err) }