├── internal ├── command │ ├── testdata │ │ └── my.keystore │ ├── kubernetes_disable.go │ ├── multi_flag.go │ ├── version.go │ ├── flag_test.go │ ├── engine.go │ ├── web.go │ ├── darwin_sdk_extract.go │ ├── ios.go │ ├── android_test.go │ ├── freebsd.go │ ├── windows_test.go │ ├── linux.go │ ├── android.go │ ├── docker.go │ ├── windows.go │ ├── container.go │ ├── darwin.go │ ├── context_test.go │ ├── context.go │ ├── flag.go │ ├── kubernetes.go │ └── command.go ├── metadata │ ├── testdata │ │ └── FyneApp.toml │ ├── load_test.go │ ├── save_test.go │ ├── save.go │ ├── load.go │ └── data.go ├── icon │ ├── icon.go │ └── fyne.go ├── resource │ └── darwin_dockerfile.go ├── log │ └── log.go ├── cmd │ └── fyne-cross-s3 │ │ └── main.go ├── cloud │ ├── kubernetes.go │ └── s3.go └── volume │ └── volume.go ├── .github ├── FUNDING.yml ├── dependabot.yaml ├── ISSUE_TEMPLATE │ ├── config.yml │ ├── feature_request.md │ └── bug_report.md ├── PULL_REQUEST_TEMPLATE.md └── workflows │ └── ci.yml ├── .gitignore ├── .idea └── modules.xml ├── main.go ├── LICENSE ├── go.mod ├── README.md └── CHANGELOG.md /internal/command/testdata/my.keystore: -------------------------------------------------------------------------------- 1 | test -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [fyne-io, Jacalz, lucor] 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /fyne-cross 2 | /internal/cmd/fyne-cross-s3/fyne-cross-s3 3 | /*.dmg 4 | /*.xip 5 | -------------------------------------------------------------------------------- /.github/dependabot.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "gomod" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | 8 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | contact_links: 2 | - name: Ask a question 3 | url: https://fyne.io/support/ 4 | about: For a toolkit question or help with your code go to our support page 5 | -------------------------------------------------------------------------------- /internal/metadata/testdata/FyneApp.toml: -------------------------------------------------------------------------------- 1 | Website = "https://apps.fyne.io" 2 | 3 | [Details] 4 | Name = "Fyne App" 5 | ID = "io.fyne.fyne" 6 | Icon = "https://conf.fyne.io/assets/img/fyne.png" 7 | Version = "v1.0" 8 | Build = 1 9 | 10 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /internal/metadata/load_test.go: -------------------------------------------------------------------------------- 1 | package metadata 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestLoadAppMetadata(t *testing.T) { 11 | r, err := os.Open("./testdata/FyneApp.toml") 12 | assert.Nil(t, err) 13 | defer r.Close() 14 | 15 | data, err := Load(r) 16 | assert.Nil(t, err) 17 | assert.Equal(t, "https://apps.fyne.io", data.Website) 18 | assert.Equal(t, "io.fyne.fyne", data.Details.ID) 19 | assert.Equal(t, "v1.0", data.Details.Version) 20 | assert.Equal(t, 1, data.Details.Build) 21 | } 22 | -------------------------------------------------------------------------------- /internal/command/kubernetes_disable.go: -------------------------------------------------------------------------------- 1 | //go:build !k8s 2 | // +build !k8s 3 | 4 | package command 5 | 6 | import ( 7 | "errors" 8 | "flag" 9 | ) 10 | 11 | var errNotImplemented error = errors.New("kubernetes support not built in. Compile fyne-cross with `-tag k8s` to enable it") 12 | 13 | func kubernetesFlagSet(_ *flag.FlagSet, _ *CommonFlags) { 14 | } 15 | 16 | func checkKubernetesClient() (err error) { 17 | return errNotImplemented 18 | } 19 | 20 | func newKubernetesContainerRunner(context Context) (containerEngine, error) { 21 | return nil, errNotImplemented 22 | } 23 | -------------------------------------------------------------------------------- /internal/command/multi_flag.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | import ( 4 | "errors" 5 | "strings" 6 | ) 7 | 8 | type multiFlags struct { 9 | values map[string]string 10 | } 11 | 12 | func (mf *multiFlags) String() string { 13 | s := "" 14 | for key, value := range mf.values { 15 | if s != "" { 16 | s += "," 17 | } 18 | s += key + "=" + value 19 | } 20 | 21 | return s 22 | } 23 | 24 | func (mf *multiFlags) Set(value string) error { 25 | splitted := strings.Split(value, "=") 26 | if len(splitted) != 2 { 27 | return errors.New("invalid metadata format, expecting key=value") 28 | } 29 | 30 | if mf.values == nil { 31 | mf.values = make(map[string]string) 32 | } 33 | 34 | mf.values[splitted[0]] = splitted[1] 35 | 36 | return nil 37 | } 38 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | 11 | 12 | ### Is your feature request related to a problem? Please describe: 13 | 14 | 15 | ### Is it possible to construct a solution with the existing API? 16 | 18 | 19 | ### Describe the solution you'd like to see: 20 | 21 | -------------------------------------------------------------------------------- /internal/metadata/save_test.go: -------------------------------------------------------------------------------- 1 | package metadata 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestSaveAppMetadata(t *testing.T) { 11 | r, err := os.Open("./testdata/FyneApp.toml") 12 | assert.Nil(t, err) 13 | data, err := Load(r) 14 | _ = r.Close() 15 | assert.Nil(t, err) 16 | assert.Equal(t, 1, data.Details.Build) 17 | 18 | data.Details.Build++ 19 | 20 | versionPath := "./testdata/Version.toml" 21 | w, err := os.Create(versionPath) 22 | assert.Nil(t, err) 23 | err = Save(data, w) 24 | assert.Nil(t, err) 25 | defer func() { 26 | os.Remove(versionPath) 27 | }() 28 | _ = w.Close() 29 | 30 | r, err = os.Open(versionPath) 31 | assert.Nil(t, err) 32 | defer r.Close() 33 | 34 | data2, err := Load(r) 35 | assert.Nil(t, err) 36 | assert.Equal(t, 2, data2.Details.Build) 37 | } 38 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 4 | 5 | ### Description: 6 | 8 | 9 | Fixes #(issue) 10 | 11 | ### Checklist: 12 | 13 | 14 | - [ ] Tests included. 15 | - [ ] Lint and formatter run with no errors. 16 | - [ ] Tests all pass. 17 | 18 | #### Where applicable: 19 | 20 | 21 | - [ ] Public APIs match existing style. 22 | - [ ] Any breaking changes have a deprecation path or have been discussed. 23 | - [ ] Updated the vendor folder (using `go mod vendor`). 24 | -------------------------------------------------------------------------------- /internal/metadata/save.go: -------------------------------------------------------------------------------- 1 | package metadata 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "os" 7 | "path/filepath" 8 | 9 | "github.com/BurntSushi/toml" 10 | ) 11 | 12 | // Save attempts to write a FyneApp metadata to the provided writer. 13 | // If the encoding fails an error will be returned. 14 | func Save(f *FyneApp, w io.Writer) error { 15 | var buf bytes.Buffer 16 | e := toml.NewEncoder(&buf) 17 | err := e.Encode(f) 18 | if err != nil { 19 | return err 20 | } 21 | 22 | _, err = w.Write(buf.Bytes()) 23 | return err 24 | } 25 | 26 | // SaveStandard attempts to save a FyneApp metadata to the `FyneApp.toml` file in the specified dir. 27 | // If the file cannot be written or encoding fails an error will be returned. 28 | func SaveStandard(f *FyneApp, dir string) error { 29 | path := filepath.Join(dir, "FyneApp.toml") 30 | w, err := os.Create(path) 31 | if err != nil { 32 | return err 33 | } 34 | 35 | defer w.Close() 36 | return Save(f, w) 37 | } 38 | -------------------------------------------------------------------------------- /internal/icon/icon.go: -------------------------------------------------------------------------------- 1 | package icon 2 | 3 | import ( 4 | "fmt" 5 | "image" 6 | "os" 7 | 8 | ico "github.com/Kodeworks/golang-image-ico" 9 | ) 10 | 11 | const ( 12 | // Default represents the default's icon name 13 | Default = "Icon.png" 14 | ) 15 | 16 | // ConvertPngToIco convert a png file to ico format 17 | func ConvertPngToIco(pngPath string, icoPath string) error { 18 | // convert icon 19 | img, err := os.Open(pngPath) 20 | if err != nil { 21 | return fmt.Errorf("failed to open source image: %s", err) 22 | } 23 | defer img.Close() 24 | srcImg, _, err := image.Decode(img) 25 | if err != nil { 26 | return fmt.Errorf("failed to decode source image: %s", err) 27 | } 28 | 29 | file, err := os.Create(icoPath) 30 | if err != nil { 31 | return fmt.Errorf("failed to open image file: %s", err) 32 | } 33 | defer file.Close() 34 | err = ico.Encode(file, srcImg) 35 | if err != nil { 36 | return fmt.Errorf("failed to write image file: %s", err) 37 | } 38 | return nil 39 | } 40 | -------------------------------------------------------------------------------- /internal/metadata/load.go: -------------------------------------------------------------------------------- 1 | package metadata 2 | 3 | import ( 4 | "io" 5 | "os" 6 | "path/filepath" 7 | 8 | "github.com/BurntSushi/toml" 9 | ) 10 | 11 | // Load attempts to read a FyneApp metadata from the provided reader. 12 | // If this cannot be done an error will be returned. 13 | func Load(r io.Reader) (*FyneApp, error) { 14 | str, err := io.ReadAll(r) 15 | if err != nil { 16 | return nil, err 17 | } 18 | 19 | var data FyneApp 20 | if _, err := toml.Decode(string(str), &data); err != nil { 21 | return nil, err 22 | } 23 | 24 | return &data, nil 25 | } 26 | 27 | // LoadStandard attempts to read a FyneApp metadata from the `FyneApp.toml` file in the specified dir. 28 | // If the file cannot be found or parsed an error will be returned. 29 | func LoadStandard(dir string) (*FyneApp, error) { 30 | path := filepath.Join(dir, "FyneApp.toml") 31 | r, err := os.Open(path) 32 | if err != nil { 33 | return nil, err 34 | } 35 | 36 | defer r.Close() 37 | return Load(r) 38 | } 39 | -------------------------------------------------------------------------------- /internal/command/version.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | import ( 4 | "fmt" 5 | "runtime/debug" 6 | ) 7 | 8 | const version = "develop" 9 | 10 | // Version is the version command 11 | type Version struct{} 12 | 13 | // Name returns the one word command name 14 | func (cmd *Version) Name() string { 15 | return "version" 16 | } 17 | 18 | // Description returns the command description 19 | func (cmd *Version) Description() string { 20 | return "Print the fyne-cross version information" 21 | } 22 | 23 | // Run runs the command 24 | func (cmd *Version) Run() error { 25 | fmt.Printf("fyne-cross version %s\n", getVersion()) 26 | return nil 27 | } 28 | 29 | // Parse parses the arguments and set the usage for the command 30 | func (cmd *Version) Parse(args []string) error { 31 | return nil 32 | } 33 | 34 | // Usage displays the command usage 35 | func (cmd *Version) Usage() { 36 | template := `Usage: fyne-cross version 37 | 38 | {{ . }} 39 | ` 40 | printUsage(template, cmd.Description()) 41 | } 42 | 43 | func getVersion() string { 44 | if info, ok := debug.ReadBuildInfo(); ok { 45 | return info.Main.Version 46 | } 47 | return version 48 | } 49 | -------------------------------------------------------------------------------- /internal/metadata/data.go: -------------------------------------------------------------------------------- 1 | package metadata 2 | 3 | // This file containts the Fyne metadata 4 | // @see https://github.com/fyne-io/fyne/blob/v2.5.3/internal/metadata/data.go 5 | 6 | // FyneApp describes the top level metadata for building a fyne application 7 | type FyneApp struct { 8 | Website string `toml:",omitempty"` 9 | Details AppDetails 10 | Development map[string]string `toml:",omitempty"` 11 | Release map[string]string `toml:",omitempty"` 12 | Source *AppSource `toml:",omitempty"` 13 | LinuxAndBSD *LinuxAndBSD `toml:",omitempty"` 14 | Languages []string `toml:",omitempty"` 15 | } 16 | 17 | // AppDetails describes the build information, this group may be OS or arch specific 18 | type AppDetails struct { 19 | Icon string `toml:",omitempty"` 20 | Name, ID string `toml:",omitempty"` 21 | Version string `toml:",omitempty"` 22 | Build int `toml:",omitempty"` 23 | } 24 | 25 | type AppSource struct { 26 | Repo, Dir string `toml:",omitempty"` 27 | } 28 | 29 | // LinuxAndBSD describes specific metadata for desktop files on Linux and BSD. 30 | type LinuxAndBSD struct { 31 | GenericName string `toml:",omitempty"` 32 | Categories []string `toml:",omitempty"` 33 | Comment string `toml:",omitempty"` 34 | Keywords []string `toml:",omitempty"` 35 | ExecParams string `toml:",omitempty"` 36 | } 37 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/fyne-io/fyne-cross/internal/command" 7 | "github.com/fyne-io/fyne-cross/internal/log" 8 | ) 9 | 10 | func main() { 11 | 12 | // Define the command to use 13 | commands := []command.Command{ 14 | &command.DarwinSDKExtract{}, 15 | command.NewDarwinCommand(), 16 | command.NewLinuxCommand(), 17 | command.NewWindowsCommand(), 18 | command.NewAndroidCommand(), 19 | command.NewIOSCommand(), 20 | command.NewFreeBSD(), 21 | command.NewWebCommand(), 22 | &command.Version{}, 23 | } 24 | 25 | // display fyne-cross usage if no command is specified 26 | if len(os.Args) == 1 { 27 | command.Usage(commands) 28 | os.Exit(1) 29 | } 30 | 31 | // check for valid command 32 | var cmd command.Command 33 | for _, v := range commands { 34 | if os.Args[1] == v.Name() { 35 | cmd = v 36 | break 37 | } 38 | } 39 | 40 | // If no valid command is specified display the usage 41 | if cmd == nil { 42 | command.Usage(commands) 43 | os.Exit(1) 44 | } 45 | 46 | // Parse the arguments for the command 47 | // It will display the command usage if -help is specified 48 | // and will exit in case of error 49 | err := cmd.Parse(os.Args[2:]) 50 | if err != nil { 51 | log.Fatalf("[✗] %s", err) 52 | } 53 | 54 | // Finally run the command 55 | err = cmd.Run() 56 | if err != nil { 57 | log.Fatalf("[✗] %s", err) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /internal/command/flag_test.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | import "testing" 4 | 5 | func Test_envFlag_Set(t *testing.T) { 6 | tests := []struct { 7 | name string 8 | value []string 9 | wantLen int 10 | wantErr bool 11 | }{ 12 | { 13 | name: "simple env var", 14 | value: []string{"CGO_ENABLED=1"}, 15 | wantLen: 1, 16 | wantErr: false, 17 | }, 18 | { 19 | name: "env var without value", 20 | value: []string{"KEY="}, 21 | wantLen: 1, 22 | wantErr: false, 23 | }, 24 | { 25 | name: "env var with value containing =", 26 | value: []string{"GOFLAGS=-mod=vendor"}, 27 | wantLen: 1, 28 | wantErr: false, 29 | }, 30 | { 31 | name: "two env vars", 32 | value: []string{"GOFLAGS=-mod=vendor", "KEY=value"}, 33 | wantLen: 2, 34 | wantErr: false, 35 | }, 36 | { 37 | name: "invalid", 38 | value: []string{"GOFLAGS"}, 39 | wantLen: 0, 40 | wantErr: true, 41 | }, 42 | { 43 | name: "env var with value containing comma", 44 | value: []string{"GOFLAGS=https://goproxy.io,direct"}, 45 | wantLen: 1, 46 | wantErr: false, 47 | }, 48 | } 49 | for _, tt := range tests { 50 | ef := &envFlag{} 51 | t.Run(tt.name, func(t *testing.T) { 52 | for _, v := range tt.value { 53 | if err := ef.Set(v); (err != nil) != tt.wantErr { 54 | t.Errorf("envFlag.Set() error = %v, wantErr %v", err, tt.wantErr) 55 | } 56 | } 57 | if len(*ef) != tt.wantLen { 58 | t.Errorf("envFlag len error = %v, wantLen %v", len(*ef), tt.wantLen) 59 | } 60 | }) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: 'unverified' 6 | assignees: '' 7 | 8 | --- 9 | 10 | 11 | 12 | ### Describe the bug: 13 | 14 | 15 | 16 | ### To Reproduce: 17 | Steps to reproduce the behaviour: 18 | 1. Go to '...' 19 | 2. Run the command '...' 20 | 3. See error 21 | 22 | ### Example code: 23 | 24 | 29 | 30 | ### Device and debug info (please complete the following information): 31 | 32 | Device info 33 | 34 | - **OS:** 35 | - **Version:** 36 | - **Go version:** 37 | - **fyne-cross version:** 38 | - **Fyne version:** 39 | 40 |
Debug info
41 | 
47 | 
-------------------------------------------------------------------------------- /internal/resource/darwin_dockerfile.go: -------------------------------------------------------------------------------- 1 | // auto-generated by cmd/internal/main.go DO NOT EDIT. 2 | 3 | package resource 4 | 5 | const DockerfileDarwin = `ARG LLVM_VERSION=14 6 | ARG OSX_VERSION_MIN=10.12 7 | ARG OSX_CROSS_COMMIT="50e86ebca7d14372febd0af8cd098705049161b9" 8 | ARG FYNE_CROSS_VERSION=1.3 9 | ARG DOCKER_REGISTRY="docker.io" 10 | 11 | ## Build osxcross toolchain 12 | FROM ${DOCKER_REGISTRY}/fyneio/fyne-cross:${FYNE_CROSS_VERSION}-base-llvm as osxcross 13 | ARG OSX_CROSS_COMMIT 14 | ARG OSX_VERSION_MIN 15 | 16 | RUN apt-get update -qq && apt-get install -y -q --no-install-recommends \ 17 | bzip2 \ 18 | cmake \ 19 | cpio \ 20 | patch \ 21 | libbz2-dev \ 22 | libssl-dev \ 23 | zlib1g-dev \ 24 | liblzma-dev \ 25 | libxml2-dev \ 26 | uuid-dev \ 27 | && rm -rf /var/lib/apt/lists/* 28 | 29 | COPY *.dmg /tmp/command_line_tools_for_xcode.dmg 30 | 31 | WORKDIR "/osxcross" 32 | 33 | RUN curl -L https://github.com/tpoechtrager/osxcross/archive/${OSX_CROSS_COMMIT}.tar.gz | tar -zx --strip-components=1 34 | 35 | RUN ./tools/gen_sdk_package_tools_dmg.sh /tmp/command_line_tools_for_xcode.dmg 36 | 37 | ARG SDK_VERSION 38 | RUN echo "Available SDKs:" && ls -1 MacOSX*.tar.* && \ 39 | if [ -z "$SDK_VERSION" ] ;\ 40 | then ls -1 MacOSX*.tar.* | sort -Vr | head -1 | xargs -i mv {} tarballs ;\ 41 | else mv MacOSX*.tar.* tarballs ; \ 42 | fi 43 | 44 | RUN UNATTENDED=yes SDK_VERSION=${SDK_VERSION} OSX_VERSION_MIN=${OSX_VERSION_MIN} ./build.sh 45 | 46 | 47 | ## Build darwin-latest image 48 | FROM ${DOCKER_REGISTRY}/fyneio/fyne-cross:${FYNE_CROSS_VERSION}-base-llvm 49 | 50 | COPY --from=osxcross /osxcross/target /osxcross/target 51 | ENV PATH=/osxcross/target/bin:$PATH 52 | ` 53 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2020 Fyne.io developers (see AUTHORS) 4 | Copyright (c) 2019, 2020 Luca Corbo and contributors (see https://github.com/lucor/fyne-cross/graphs/contributors) 5 | 6 | All rights reserved. 7 | 8 | Redistribution and use in source and binary forms, with or without 9 | modification, are permitted provided that the following conditions are met: 10 | 11 | 1. Redistributions of source code must retain the above copyright notice, this 12 | list of conditions and the following disclaimer. 13 | 14 | 2. Redistributions in binary form must reproduce the above copyright notice, 15 | this list of conditions and the following disclaimer in the documentation 16 | and/or other materials provided with the distribution. 17 | 18 | 3. Neither the name of the copyright holder nor the names of its 19 | contributors may be used to endorse or promote products derived from 20 | this software without specific prior written permission. 21 | 22 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 23 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 24 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 25 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 26 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 27 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 28 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 29 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 30 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 31 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 32 | -------------------------------------------------------------------------------- /internal/command/engine.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "strings" 7 | 8 | "golang.org/x/sys/execabs" 9 | ) 10 | 11 | const ( 12 | autodetectEngine = "" 13 | dockerEngine = "docker" 14 | podmanEngine = "podman" 15 | kubernetesEngine = "kubernetes" 16 | ) 17 | 18 | type Engine struct { 19 | Name string 20 | Binary string 21 | } 22 | 23 | func (e Engine) String() string { 24 | return e.Name 25 | } 26 | 27 | func (e Engine) IsDocker() bool { 28 | return e.Name == dockerEngine 29 | } 30 | 31 | func (e Engine) IsPodman() bool { 32 | return e.Name == podmanEngine 33 | } 34 | 35 | func (e Engine) IsKubernetes() bool { 36 | return e.Name == kubernetesEngine 37 | } 38 | 39 | // MakeEngine returns a new container engine. Pass empty string to autodetect 40 | func MakeEngine(e string) (Engine, error) { 41 | switch e { 42 | case dockerEngine: 43 | binaryPath, err := execabs.LookPath(dockerEngine) 44 | if err != nil { 45 | return Engine{}, fmt.Errorf("docker binary not found in PATH") 46 | } 47 | return Engine{Name: dockerEngine, Binary: binaryPath}, nil 48 | case podmanEngine: 49 | binaryPath, err := execabs.LookPath(podmanEngine) 50 | if err != nil { 51 | return Engine{}, fmt.Errorf("podman binary not found in PATH") 52 | } 53 | return Engine{Name: podmanEngine, Binary: binaryPath}, nil 54 | case "": 55 | binaryPath, err := execabs.LookPath(dockerEngine) 56 | if err != nil { 57 | // check for podman engine 58 | binaryPath, err := execabs.LookPath(podmanEngine) 59 | if err != nil { 60 | return Engine{}, fmt.Errorf("engine binary not found in PATH") 61 | } 62 | return Engine{Name: podmanEngine, Binary: binaryPath}, nil 63 | } 64 | // docker binary found, check if it is an alias to podman 65 | // if "docker" comes from an alias (i.e. "podman-docker") should not contain the "docker" string 66 | out, err := execabs.Command(binaryPath, "--version").Output() 67 | if err != nil { 68 | return Engine{}, fmt.Errorf("could not detect engine version: %s", out) 69 | } 70 | lout := strings.ToLower(string(out)) 71 | switch { 72 | case strings.Contains(lout, dockerEngine): 73 | return Engine{Name: dockerEngine, Binary: binaryPath}, nil 74 | case strings.Contains(lout, podmanEngine): 75 | return Engine{Name: podmanEngine, Binary: binaryPath}, nil 76 | default: 77 | return Engine{}, fmt.Errorf("could not detect engine version: %s", out) 78 | } 79 | case kubernetesEngine: 80 | // Try establishing a connection to Kubernetes cluster 81 | err := checkKubernetesClient() 82 | if err != nil { 83 | return Engine{}, err 84 | } 85 | 86 | return Engine{Name: kubernetesEngine, Binary: ""}, nil 87 | default: 88 | return Engine{}, errors.New("unsupported container engine") 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /internal/log/log.go: -------------------------------------------------------------------------------- 1 | // Package log implements a simple logging package with severity level. 2 | package log 3 | 4 | import ( 5 | "io" 6 | golog "log" 7 | "os" 8 | "sync" 9 | "text/template" 10 | ) 11 | 12 | // Level defines the log verbosity 13 | type Level int 14 | 15 | const ( 16 | // LevelSilent is the silent level. Use to silent log events. 17 | LevelSilent Level = iota 18 | // LevelInfo is the info level. Use to log interesting events. 19 | LevelInfo 20 | // LevelDebug is the debug level. Use to log detailed debug information. 21 | LevelDebug 22 | ) 23 | 24 | // logger is the logger 25 | type logger struct { 26 | *golog.Logger 27 | 28 | mu sync.Mutex 29 | level Level 30 | } 31 | 32 | // SetLevel sets the logger level 33 | func (l *logger) SetLevel(level Level) { 34 | l.mu.Lock() 35 | defer l.mu.Unlock() 36 | l.level = level 37 | } 38 | 39 | // std is the default logger 40 | var std = &logger{ 41 | Logger: golog.New(os.Stderr, "", 0), 42 | level: LevelInfo, 43 | } 44 | 45 | // Debug logs a debug information 46 | // Arguments are handled in the manner of fmt.Print 47 | func Debug(v ...interface{}) { 48 | if std.level < LevelDebug { 49 | return 50 | } 51 | std.Print(v...) 52 | } 53 | 54 | // Debugf logs a debug information 55 | // Arguments are handled in the manner of fmt.Printf 56 | func Debugf(format string, v ...interface{}) { 57 | if std.level < LevelDebug { 58 | return 59 | } 60 | std.Printf(format, v...) 61 | } 62 | 63 | // Info logs an information 64 | // Arguments are handled in the manner of fmt.Print 65 | func Info(v ...interface{}) { 66 | if std.level < LevelInfo { 67 | return 68 | } 69 | std.Print(v...) 70 | } 71 | 72 | // Infof logs an information 73 | // Arguments are handled in the manner of fmt.Printf 74 | func Infof(format string, v ...interface{}) { 75 | if std.level < LevelInfo { 76 | return 77 | } 78 | std.Printf(format, v...) 79 | } 80 | 81 | // Fatal logs a fatal event and exit 82 | // Arguments are handled in the manner of fmt.Print followed by a call to os.Exit(1) 83 | func Fatal(v ...interface{}) { 84 | std.Fatal(v...) 85 | } 86 | 87 | // Fatalf logs a fatal event and exit 88 | // Arguments are handled in the manner of fmt.Printf followed by a call to os.Exit(1) 89 | func Fatalf(format string, v ...interface{}) { 90 | std.Fatalf(format, v...) 91 | } 92 | 93 | // PrintTemplate prints the parsed text template to the specified data object, 94 | // and writes the output to w. 95 | func PrintTemplate(w io.Writer, textTemplate string, data interface{}) { 96 | tpl, err := template.New("tpl").Parse(textTemplate) 97 | if err != nil { 98 | golog.Fatalf("Could not parse the template: %s", err) 99 | } 100 | err = tpl.Execute(w, data) 101 | if err != nil { 102 | golog.Fatalf("Could not execute the template: %s", err) 103 | } 104 | } 105 | 106 | // SetLevel sets the logger level 107 | func SetLevel(level Level) { 108 | std.SetLevel(level) 109 | } 110 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/fyne-io/fyne-cross 2 | 3 | go 1.19 4 | 5 | require ( 6 | github.com/BurntSushi/toml v1.5.0 7 | github.com/Kodeworks/golang-image-ico v0.0.0-20141118225523-73f0f4cfade9 8 | github.com/aws/aws-sdk-go v1.55.8 9 | github.com/klauspost/compress v1.13.4 10 | github.com/mholt/archiver/v3 v3.5.1 11 | github.com/stretchr/testify v1.10.0 12 | github.com/urfave/cli/v2 v2.27.7 13 | golang.org/x/sync v0.6.0 14 | golang.org/x/sys v0.18.0 15 | k8s.io/api v0.28.15 16 | k8s.io/apimachinery v0.28.15 17 | k8s.io/client-go v0.28.15 18 | k8s.io/kubectl v0.28.15 19 | ) 20 | 21 | require ( 22 | github.com/andybalholm/brotli v1.0.4 // indirect 23 | github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect 24 | github.com/davecgh/go-spew v1.1.1 // indirect 25 | github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5 // indirect 26 | github.com/emicklei/go-restful/v3 v3.11.0 // indirect 27 | github.com/go-logr/logr v1.4.1 // indirect 28 | github.com/go-openapi/jsonpointer v0.19.6 // indirect 29 | github.com/go-openapi/jsonreference v0.20.2 // indirect 30 | github.com/go-openapi/swag v0.22.3 // indirect 31 | github.com/gogo/protobuf v1.3.2 // indirect 32 | github.com/golang/protobuf v1.5.4 // indirect 33 | github.com/golang/snappy v0.0.4 // indirect 34 | github.com/google/gnostic-models v0.6.8 // indirect 35 | github.com/google/gofuzz v1.2.0 // indirect 36 | github.com/google/uuid v1.3.0 // indirect 37 | github.com/imdario/mergo v0.3.6 // indirect 38 | github.com/jmespath/go-jmespath v0.4.0 // indirect 39 | github.com/josharian/intern v1.0.0 // indirect 40 | github.com/json-iterator/go v1.1.12 // indirect 41 | github.com/klauspost/pgzip v1.2.5 // indirect 42 | github.com/mailru/easyjson v0.7.7 // indirect 43 | github.com/moby/spdystream v0.2.0 // indirect 44 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 45 | github.com/modern-go/reflect2 v1.0.2 // indirect 46 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 47 | github.com/nwaples/rardecode v1.1.0 // indirect 48 | github.com/pierrec/lz4/v4 v4.1.14 // indirect 49 | github.com/pmezard/go-difflib v1.0.0 // indirect 50 | github.com/russross/blackfriday/v2 v2.1.0 // indirect 51 | github.com/spf13/pflag v1.0.5 // indirect 52 | github.com/ulikunitz/xz v0.5.10 // indirect 53 | github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect 54 | github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect 55 | golang.org/x/net v0.23.0 // indirect 56 | golang.org/x/oauth2 v0.10.0 // indirect 57 | golang.org/x/term v0.18.0 // indirect 58 | golang.org/x/text v0.14.0 // indirect 59 | golang.org/x/time v0.3.0 // indirect 60 | google.golang.org/appengine v1.6.7 // indirect 61 | google.golang.org/protobuf v1.33.0 // indirect 62 | gopkg.in/inf.v0 v0.9.1 // indirect 63 | gopkg.in/yaml.v2 v2.4.0 // indirect 64 | gopkg.in/yaml.v3 v3.0.1 // indirect 65 | k8s.io/klog/v2 v2.120.1 // indirect 66 | k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 // indirect 67 | k8s.io/utils v0.0.0-20230726121419-3b25d923346b // indirect 68 | sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect 69 | sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect 70 | sigs.k8s.io/yaml v1.3.0 // indirect 71 | ) 72 | -------------------------------------------------------------------------------- /internal/command/web.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/fyne-io/fyne-cross/internal/log" 7 | "github.com/fyne-io/fyne-cross/internal/volume" 8 | ) 9 | 10 | const ( 11 | // webOS it the ios OS name 12 | webOS = "web" 13 | // webImage is the fyne-cross image for the web 14 | webImage = "fyneio/fyne-cross-images:web" 15 | ) 16 | 17 | // web build and package the fyne app for the web 18 | type web struct { 19 | Images []containerImage 20 | defaultContext Context 21 | } 22 | 23 | var ( 24 | _ platformBuilder = (*web)(nil) 25 | _ Command = (*web)(nil) 26 | ) 27 | 28 | func NewWebCommand() *web { 29 | return &web{} 30 | } 31 | 32 | // Name returns the one word command name 33 | func (cmd *web) Name() string { 34 | return "web" 35 | } 36 | 37 | // Description returns the command description 38 | func (cmd *web) Description() string { 39 | return "Build and package a fyne application for the web" 40 | } 41 | 42 | func (cmd *web) Run() error { 43 | return commonRun(cmd.defaultContext, cmd.Images, cmd) 44 | } 45 | 46 | // Parse parses the arguments and set the usage for the command 47 | func (cmd *web) Parse(args []string) error { 48 | commonFlags, err := newCommonFlags() 49 | if err != nil { 50 | return err 51 | } 52 | 53 | flags := &webFlags{ 54 | CommonFlags: commonFlags, 55 | } 56 | 57 | flagSet.Usage = cmd.Usage 58 | flagSet.Parse(args) 59 | 60 | return cmd.setupContainerImages(flags, flagSet.Args()) 61 | } 62 | 63 | // Run runs the command 64 | func (cmd *web) Build(image containerImage) (string, error) { 65 | log.Info("[i] Packaging app...") 66 | 67 | err := prepareIcon(cmd.defaultContext, image) 68 | if err != nil { 69 | return "", err 70 | } 71 | 72 | image.SetEnv("CGO_ENABLED", "0") 73 | if cmd.defaultContext.Release { 74 | // Release mode 75 | err = fyneRelease(cmd.defaultContext, image) 76 | } else { 77 | // Build mode 78 | err = fynePackage(cmd.defaultContext, image) 79 | } 80 | if err != nil { 81 | return "", fmt.Errorf("could not package the Fyne app: %v", err) 82 | } 83 | 84 | // move the dist package into the "tmp" folder 85 | srcFile := volume.JoinPathContainer(cmd.defaultContext.WorkDirContainer(), "wasm") 86 | dstFile := volume.JoinPathContainer(cmd.defaultContext.TmpDirContainer(), image.ID()) 87 | return "", image.Run(cmd.defaultContext.Volume, options{}, []string{"mv", srcFile, dstFile}) 88 | } 89 | 90 | // Usage displays the command usage 91 | func (cmd *web) Usage() { 92 | data := struct { 93 | Name string 94 | Description string 95 | }{ 96 | Name: cmd.Name(), 97 | Description: cmd.Description(), 98 | } 99 | 100 | template := ` 101 | Usage: fyne-cross {{ .Name }} [options] [package] 102 | 103 | {{ .Description }} 104 | 105 | Note: available only on darwin hosts 106 | 107 | Options: 108 | ` 109 | 110 | printUsage(template, data) 111 | flagSet.PrintDefaults() 112 | } 113 | 114 | // webFlags defines the command-line flags for the web command 115 | type webFlags struct { 116 | *CommonFlags 117 | } 118 | 119 | // makeWebContext returns the command context for an iOS target 120 | func (cmd *web) setupContainerImages(flags *webFlags, args []string) error { 121 | ctx, err := makeDefaultContext(flags.CommonFlags, args) 122 | if err != nil { 123 | return err 124 | } 125 | 126 | cmd.defaultContext = ctx 127 | runner, err := newContainerEngine(ctx) 128 | if err != nil { 129 | return err 130 | } 131 | 132 | image := runner.createContainerImage("", webOS, overrideDockerImage(flags.CommonFlags, webImage)) 133 | cmd.Images = append(cmd.Images, image) 134 | 135 | return nil 136 | } 137 | -------------------------------------------------------------------------------- /internal/command/darwin_sdk_extract.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "os" 7 | "path/filepath" 8 | "strings" 9 | 10 | "github.com/fyne-io/fyne-cross/internal/log" 11 | "github.com/fyne-io/fyne-cross/internal/volume" 12 | ) 13 | 14 | const ( 15 | darwinSDKExtractImage = "fyneio/fyne-cross-images:darwin-sdk-extractor" 16 | darwinSDKExtractOutDir = "SDKs" 17 | darwinSDKExtractScript = "darwin-sdk-extractor.sh" 18 | ) 19 | 20 | // DarwinSDKExtract extracts the macOS SDK from the Command Line Tools for Xcode package 21 | type DarwinSDKExtract struct { 22 | pull bool 23 | sdkPath string 24 | containerEngine string 25 | } 26 | 27 | // Name returns the one word command name 28 | func (cmd *DarwinSDKExtract) Name() string { 29 | return "darwin-sdk-extract" 30 | } 31 | 32 | // Description returns the command description 33 | func (cmd *DarwinSDKExtract) Description() string { 34 | return "Extracts the macOS SDK from the Command Line Tools for Xcode package" 35 | } 36 | 37 | // Parse parses the arguments and set the usage for the command 38 | func (cmd *DarwinSDKExtract) Parse(args []string) error { 39 | 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)") 40 | // flagSet.StringVar(&cmd.sdkVersion, "sdk-version", "", "SDK version to use. Default to automatic detection") 41 | flagSet.StringVar(&cmd.containerEngine, "engine", "", "The container engine to use. Supported engines: [docker, podman]. Default to autodetect.") 42 | flagSet.BoolVar(&cmd.pull, "pull", true, "Attempt to pull a newer version of the docker base image") 43 | 44 | flagSet.Usage = cmd.Usage 45 | flagSet.Parse(args) 46 | 47 | if cmd.sdkPath == "" { 48 | return fmt.Errorf("path to the Command Line Tools for Xcode using the 'xcode-path' is required.\nRun 'fyne-cross %s --help' for details", cmd.Name()) 49 | } 50 | 51 | i, err := os.Stat(cmd.sdkPath) 52 | if os.IsNotExist(err) { 53 | return fmt.Errorf("Command Line Tools for Xcode file %q does not exists", cmd.sdkPath) 54 | } 55 | if err != nil { 56 | return fmt.Errorf("Command Line Tools for Xcode file %q error: %s", cmd.sdkPath, err) 57 | } 58 | if i.IsDir() { 59 | return fmt.Errorf("Command Line Tools for Xcode file %q is a directory", cmd.sdkPath) 60 | } 61 | if !strings.HasSuffix(cmd.sdkPath, ".dmg") { 62 | return fmt.Errorf("Command Line Tools for Xcode file must be in dmg format") 63 | } 64 | 65 | return nil 66 | } 67 | 68 | // Run runs the command 69 | func (cmd *DarwinSDKExtract) Run() error { 70 | 71 | sdkDir := filepath.Dir(cmd.sdkPath) 72 | dmg := filepath.Base(cmd.sdkPath) 73 | outDir := filepath.Join(sdkDir, darwinSDKExtractOutDir) 74 | 75 | if _, err := os.Stat(outDir); !errors.Is(err, os.ErrNotExist) { 76 | return fmt.Errorf("output dir %q already exists. Remove before continue", outDir) 77 | } 78 | 79 | // mount the fyne-cross volume 80 | workDir, err := os.MkdirTemp("", cmd.Name()) 81 | if err != nil { 82 | return err 83 | } 84 | 85 | vol, err := volume.Mount(workDir, "") 86 | if err != nil { 87 | return err 88 | } 89 | 90 | // attempt to autodetect 91 | containerEngine, err := MakeEngine(cmd.containerEngine) 92 | if err != nil { 93 | return err 94 | } 95 | 96 | ctx := Context{ 97 | Engine: containerEngine, 98 | Debug: true, 99 | Pull: cmd.pull, 100 | Volume: vol, 101 | } 102 | 103 | engine, err := newLocalContainerEngine(ctx) 104 | if err != nil { 105 | return err 106 | } 107 | 108 | i := engine.createContainerImage("", linuxOS, darwinSDKExtractImage) 109 | i.SetMount("sdk", sdkDir, "/mnt") 110 | i.Prepare() 111 | 112 | log.Infof("[i] Extracting SDKs from %q, please wait it could take a while...", dmg) 113 | err = i.Run(ctx.Volume, options{}, []string{ 114 | darwinSDKExtractScript, 115 | dmg, 116 | }) 117 | if err != nil { 118 | return err 119 | } 120 | log.Infof("[✓] SDKs extracted to: %s", outDir) 121 | return nil 122 | } 123 | 124 | // Usage displays the command usage 125 | func (cmd *DarwinSDKExtract) Usage() { 126 | data := struct { 127 | Name string 128 | Description string 129 | }{ 130 | Name: cmd.Name(), 131 | Description: cmd.Description(), 132 | } 133 | 134 | template := ` 135 | Usage: fyne-cross {{ .Name }} [options] 136 | 137 | {{ .Description }} 138 | 139 | Options: 140 | ` 141 | 142 | printUsage(template, data) 143 | flagSet.PrintDefaults() 144 | } 145 | -------------------------------------------------------------------------------- /internal/command/ios.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | import ( 4 | "fmt" 5 | "runtime" 6 | 7 | "github.com/fyne-io/fyne-cross/internal/log" 8 | "github.com/fyne-io/fyne-cross/internal/volume" 9 | ) 10 | 11 | const ( 12 | // iosOS it the ios OS name 13 | iosOS = "ios" 14 | // iosImage is the fyne-cross image for the iOS OS 15 | iosImage = "fyneio/fyne-cross:1.3-base" 16 | ) 17 | 18 | // IOS build and package the fyne app for the ios OS 19 | type iOS struct { 20 | Images []containerImage 21 | defaultContext Context 22 | } 23 | 24 | var _ platformBuilder = (*iOS)(nil) 25 | var _ Command = (*iOS)(nil) 26 | 27 | func NewIOSCommand() *iOS { 28 | return &iOS{} 29 | } 30 | 31 | func (cmd *iOS) Name() string { 32 | return "ios" 33 | } 34 | 35 | // Description returns the command description 36 | func (cmd *iOS) Description() string { 37 | return "Build and package a fyne application for the iOS OS" 38 | } 39 | 40 | func (cmd *iOS) Run() error { 41 | return commonRun(cmd.defaultContext, cmd.Images, cmd) 42 | } 43 | 44 | // Parse parses the arguments and set the usage for the command 45 | func (cmd *iOS) Parse(args []string) error { 46 | commonFlags, err := newCommonFlags() 47 | if err != nil { 48 | return err 49 | } 50 | 51 | flags := &iosFlags{ 52 | CommonFlags: commonFlags, 53 | } 54 | 55 | // flags used only in release mode 56 | flagSet.StringVar(&flags.Certificate, "certificate", "", "The name of the certificate to sign the build") 57 | flagSet.StringVar(&flags.Profile, "profile", "", "The name of the provisioning profile for this release build") 58 | 59 | flagAppID := flagSet.Lookup("app-id") 60 | flagAppID.Usage = fmt.Sprintf("%s. Must match a valid provisioning profile [required]", flagAppID.Usage) 61 | 62 | flagSet.Usage = cmd.Usage 63 | flagSet.Parse(args) 64 | 65 | err = cmd.setupContainerImages(flags, flagSet.Args()) 66 | return err 67 | } 68 | 69 | // Run runs the command 70 | func (cmd *iOS) Build(image containerImage) (string, error) { 71 | err := prepareIcon(cmd.defaultContext, image) 72 | if err != nil { 73 | return "", err 74 | } 75 | 76 | log.Info("[i] Packaging app...") 77 | 78 | var packageName string 79 | if cmd.defaultContext.Release { 80 | // Release mode 81 | packageName, err = fyneReleaseHost(cmd.defaultContext, image) 82 | } else { 83 | // Build mode 84 | packageName, err = fynePackageHost(cmd.defaultContext, image) 85 | } 86 | 87 | if err != nil { 88 | return "", fmt.Errorf("could not package the Fyne app: %v", err) 89 | } 90 | 91 | // move the dist package into the expected tmp/$ID/packageName location in the container 92 | image.Run(cmd.defaultContext.Volume, options{}, []string{ 93 | "sh", "-c", fmt.Sprintf("mv %q/*.ipa %q", 94 | cmd.defaultContext.WorkDirContainer(), 95 | volume.JoinPathContainer(cmd.defaultContext.TmpDirContainer(), image.ID(), packageName)), 96 | }) 97 | 98 | return packageName, nil 99 | } 100 | 101 | // Usage displays the command usage 102 | func (cmd *iOS) Usage() { 103 | data := struct { 104 | Name string 105 | Description string 106 | }{ 107 | Name: cmd.Name(), 108 | Description: cmd.Description(), 109 | } 110 | 111 | template := ` 112 | Usage: fyne-cross {{ .Name }} [options] [package] 113 | 114 | {{ .Description }} 115 | 116 | Note: available only on darwin hosts 117 | 118 | Options: 119 | ` 120 | 121 | printUsage(template, data) 122 | flagSet.PrintDefaults() 123 | } 124 | 125 | // iosFlags defines the command-line flags for the ios command 126 | type iosFlags struct { 127 | *CommonFlags 128 | 129 | //Certificate represents the name of the certificate to sign the build 130 | Certificate string 131 | 132 | //Profile represents the name of the provisioning profile for this release build 133 | Profile string 134 | } 135 | 136 | // setupContainerImages returns the command ContainerImages for an iOS target 137 | func (cmd *iOS) setupContainerImages(flags *iosFlags, args []string) error { 138 | if runtime.GOOS != darwinOS { 139 | return fmt.Errorf("iOS build is supported only on darwin hosts") 140 | } 141 | 142 | ctx, err := makeDefaultContext(flags.CommonFlags, args) 143 | if err != nil { 144 | return err 145 | } 146 | 147 | // appID is mandatory for ios 148 | if ctx.AppID == "" { 149 | return fmt.Errorf("appID is mandatory for %s", iosImage) 150 | } 151 | 152 | ctx.Certificate = flags.Certificate 153 | ctx.Profile = flags.Profile 154 | 155 | cmd.defaultContext = ctx 156 | runner, err := newContainerEngine(ctx) 157 | if err != nil { 158 | return err 159 | } 160 | 161 | cmd.Images = append(cmd.Images, runner.createContainerImage("", iosOS, overrideDockerImage(flags.CommonFlags, iosImage))) 162 | 163 | return nil 164 | } 165 | -------------------------------------------------------------------------------- /internal/command/android_test.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func Test_makeAndroidContext(t *testing.T) { 11 | vol, err := mockDefaultVolume() 12 | require.Nil(t, err) 13 | 14 | engine, err := MakeEngine(autodetectEngine) 15 | if err != nil { 16 | t.Skip("engine not found", err) 17 | } 18 | 19 | type args struct { 20 | flags *androidFlags 21 | args []string 22 | } 23 | tests := []struct { 24 | name string 25 | args args 26 | wantContext Context 27 | wantImages []containerImage 28 | wantErr bool 29 | }{ 30 | { 31 | name: "keystore path must be relative to project root", 32 | args: args{ 33 | flags: &androidFlags{ 34 | CommonFlags: &CommonFlags{ 35 | AppBuild: 1, 36 | AppID: "com.example.test", 37 | }, 38 | Keystore: "/tmp/my.keystore", 39 | TargetArch: &targetArchFlag{string(ArchMultiple)}, 40 | }, 41 | }, 42 | wantContext: Context{}, 43 | wantImages: []containerImage{}, 44 | wantErr: true, 45 | }, 46 | { 47 | name: "keystore path does not exist", 48 | args: args{ 49 | flags: &androidFlags{ 50 | CommonFlags: &CommonFlags{ 51 | AppBuild: 1, 52 | AppID: "com.example.test", 53 | }, 54 | Keystore: "my.keystore", 55 | TargetArch: &targetArchFlag{string(ArchMultiple)}, 56 | }, 57 | }, 58 | wantContext: Context{}, 59 | wantImages: []containerImage{}, 60 | wantErr: true, 61 | }, 62 | { 63 | name: "default", 64 | args: args{ 65 | flags: &androidFlags{ 66 | CommonFlags: &CommonFlags{ 67 | AppBuild: 1, 68 | AppID: "com.example.test", 69 | }, 70 | Keystore: "testdata/my.keystore", 71 | TargetArch: &targetArchFlag{string(ArchMultiple)}, 72 | }, 73 | }, 74 | wantContext: Context{ 75 | AppBuild: "1", 76 | AppID: "com.example.test", 77 | Volume: vol, 78 | CacheEnabled: true, 79 | StripDebug: true, 80 | Package: ".", 81 | Keystore: "/app/testdata/my.keystore", 82 | Engine: engine, 83 | Env: map[string]string{}, 84 | }, 85 | wantImages: []containerImage{ 86 | &localContainerImage{ 87 | baseContainerImage: baseContainerImage{ 88 | arch: ArchMultiple, 89 | os: androidOS, 90 | id: androidOS, 91 | env: map[string]string{}, 92 | mount: []containerMountPoint{ 93 | {"project", vol.WorkDirHost(), vol.WorkDirContainer()}, 94 | {"cache", vol.CacheDirHost(), vol.CacheDirContainer()}, 95 | }, 96 | DockerImage: androidImage, 97 | }, 98 | }, 99 | }, 100 | wantErr: false, 101 | }, 102 | { 103 | name: "default", 104 | args: args{ 105 | flags: &androidFlags{ 106 | CommonFlags: &CommonFlags{ 107 | AppBuild: 1, 108 | AppID: "com.example.test", 109 | }, 110 | Keystore: "./testdata/my.keystore", 111 | TargetArch: &targetArchFlag{string(ArchMultiple)}, 112 | }, 113 | }, 114 | wantContext: Context{ 115 | AppBuild: "1", 116 | AppID: "com.example.test", 117 | Volume: vol, 118 | CacheEnabled: true, 119 | StripDebug: true, 120 | Package: ".", 121 | Keystore: "/app/testdata/my.keystore", 122 | Engine: engine, 123 | Env: map[string]string{}, 124 | }, 125 | wantImages: []containerImage{ 126 | &localContainerImage{ 127 | baseContainerImage: baseContainerImage{ 128 | arch: ArchMultiple, 129 | os: androidOS, 130 | id: androidOS, 131 | env: map[string]string{}, 132 | mount: []containerMountPoint{ 133 | {"project", vol.WorkDirHost(), vol.WorkDirContainer()}, 134 | {"cache", vol.CacheDirHost(), vol.CacheDirContainer()}, 135 | }, 136 | DockerImage: androidImage, 137 | }, 138 | }, 139 | }, 140 | wantErr: false, 141 | }, 142 | { 143 | name: "appID is mandatory", 144 | args: args{ 145 | flags: &androidFlags{ 146 | CommonFlags: &CommonFlags{ 147 | AppBuild: 1, 148 | }, 149 | Keystore: "./testdata/my.keystore", 150 | TargetArch: &targetArchFlag{string(ArchMultiple)}, 151 | }, 152 | }, 153 | wantContext: Context{}, 154 | wantImages: []containerImage{}, 155 | wantErr: true, 156 | }, 157 | } 158 | for _, tt := range tests { 159 | t.Run(tt.name, func(t *testing.T) { 160 | android := NewAndroidCommand() 161 | 162 | err := android.setupContainerImages(tt.args.flags, tt.args.args) 163 | 164 | if tt.wantErr { 165 | require.NotNil(t, err) 166 | return 167 | } 168 | require.Nil(t, err) 169 | assert.Equal(t, tt.wantContext, android.defaultContext) 170 | 171 | for index := range android.Images { 172 | android.Images[index].(*localContainerImage).runner = nil 173 | } 174 | 175 | assert.Equal(t, tt.wantImages, android.Images) 176 | }) 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /internal/cmd/fyne-cross-s3/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "os" 7 | 8 | "github.com/fyne-io/fyne-cross/internal/cloud" 9 | "github.com/urfave/cli/v2" 10 | ) 11 | 12 | func main() { 13 | var endpoint string 14 | var region string 15 | var bucket string 16 | var akid string 17 | var secret string 18 | var debug bool 19 | 20 | app := &cli.App{ 21 | Name: "fyne-cross-s3", 22 | Usage: "Upload and download to S3 bucket specified in the environment", 23 | Flags: []cli.Flag{ 24 | &cli.StringFlag{ 25 | Name: "aws-endpoint", 26 | Aliases: []string{"e"}, 27 | Usage: "AWS endpoint to connect to (can be used to connect to non AWS S3 services)", 28 | EnvVars: []string{"AWS_S3_ENDPOINT"}, 29 | Destination: &endpoint, 30 | }, 31 | &cli.StringFlag{ 32 | Name: "aws-region", 33 | Aliases: []string{"r"}, 34 | Usage: "AWS region to connect to", 35 | EnvVars: []string{"AWS_S3_REGION"}, 36 | Destination: ®ion, 37 | }, 38 | &cli.StringFlag{ 39 | Name: "aws-bucket", 40 | Aliases: []string{"b"}, 41 | Usage: "AWS bucket to store data into", 42 | EnvVars: []string{"AWS_S3_BUCKET"}, 43 | Destination: &bucket, 44 | }, 45 | &cli.StringFlag{ 46 | Name: "aws-secret", 47 | Aliases: []string{"s"}, 48 | Usage: "AWS secret to use to establish S3 connection", 49 | Destination: &secret, 50 | }, 51 | &cli.StringFlag{ 52 | Name: "aws-AKID", 53 | Aliases: []string{"a"}, 54 | Usage: "AWS Access Key ID to use to establish S3 connection", 55 | Destination: &akid, 56 | }, 57 | &cli.BoolFlag{ 58 | Name: "debug", 59 | Aliases: []string{"d"}, 60 | Usage: "Enable debug output", 61 | Destination: &debug, 62 | }, 63 | }, 64 | Commands: []*cli.Command{ 65 | { 66 | Name: "upload-directory", 67 | Usage: "Upload specified directory as an archive to the specified destination in S3 bucket", 68 | Action: func(c *cli.Context) error { 69 | if c.Args().Len() != 2 { 70 | return fmt.Errorf("directory to archive and destination should be specified") 71 | } 72 | 73 | if debug { 74 | cloud.Log = log.Printf 75 | } 76 | 77 | log.Println("Connecting to AWS") 78 | aws, err := cloud.NewAWSSession(akid, secret, endpoint, region, bucket) 79 | if err != nil { 80 | return err 81 | } 82 | 83 | log.Println("Uploading directory", c.Args().Get(0), "to", c.Args().Get(1)) 84 | err = aws.UploadCompressedDirectory(c.Args().Get(0), c.Args().Get(1)) 85 | if err != nil { 86 | return err 87 | } 88 | 89 | return nil 90 | }, 91 | }, 92 | { 93 | Name: "upload-file", 94 | Usage: "Upload specified file to S3 bucket", 95 | Action: func(c *cli.Context) error { 96 | if c.Args().Len() != 2 { 97 | return fmt.Errorf("file to upload and destination should be specified") 98 | } 99 | 100 | if debug { 101 | cloud.Log = log.Printf 102 | } 103 | 104 | log.Println("Connecting to AWS") 105 | aws, err := cloud.NewAWSSession(akid, secret, endpoint, region, bucket) 106 | if err != nil { 107 | return err 108 | } 109 | 110 | log.Println("Uploading file", c.Args().Get(0), "to", c.Args().Get(1)) 111 | err = aws.UploadFile(c.Args().Get(0), c.Args().Get(1)) 112 | if err != nil { 113 | return err 114 | } 115 | 116 | return nil 117 | }, 118 | }, 119 | { 120 | Name: "download-directory", 121 | Usage: "Download archive from specified S3 bucket to be expanded in a specified directory", 122 | Action: func(c *cli.Context) error { 123 | if c.Args().Len() != 2 { 124 | return fmt.Errorf("archive to download and destination should be specified") 125 | } 126 | 127 | if debug { 128 | cloud.Log = log.Printf 129 | } 130 | 131 | log.Println("Connecting to AWS") 132 | aws, err := cloud.NewAWSSession(akid, secret, endpoint, region, bucket) 133 | if err != nil { 134 | return err 135 | } 136 | 137 | log.Println("Download", c.Args().Get(0), "to directory", c.Args().Get(1)) 138 | err = aws.DownloadCompressedDirectory(c.Args().Get(0), c.Args().Get(1)) 139 | if err != nil { 140 | return err 141 | } 142 | 143 | return nil 144 | }, 145 | }, 146 | { 147 | Name: "download-file", 148 | Usage: "Download specified file from S3 bucket to be deposited at specified local destination", 149 | Action: func(c *cli.Context) error { 150 | if c.Args().Len() != 2 { 151 | return fmt.Errorf("file to upload and destination should be specified") 152 | } 153 | 154 | if debug { 155 | cloud.Log = log.Printf 156 | } 157 | 158 | log.Println("Connecting to AWS") 159 | aws, err := cloud.NewAWSSession(akid, secret, endpoint, region, bucket) 160 | if err != nil { 161 | return err 162 | } 163 | 164 | log.Println("Downloading file from", c.Args().Get(0), "to", c.Args().Get(1)) 165 | err = aws.DownloadFile(c.Args().Get(0), c.Args().Get(1)) 166 | if err != nil { 167 | return err 168 | } 169 | 170 | return nil 171 | }, 172 | }, 173 | }} 174 | 175 | err := app.Run(os.Args) 176 | if err != nil { 177 | log.Fatal(err) 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /internal/command/freebsd.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | import ( 4 | "fmt" 5 | "runtime" 6 | 7 | "github.com/fyne-io/fyne-cross/internal/log" 8 | "github.com/fyne-io/fyne-cross/internal/volume" 9 | ) 10 | 11 | const ( 12 | // freebsdOS it the freebsd OS name 13 | freebsdOS = "freebsd" 14 | // freebsdImageAmd64 is the fyne-cross image for the FreeBSD OS amd64 arch 15 | freebsdImageAmd64 = "fyneio/fyne-cross-images:freebsd-amd64" 16 | // freebsdImageArm64 is the fyne-cross image for the FreeBSD OS arm64 arch 17 | freebsdImageArm64 = "fyneio/fyne-cross-images:freebsd-arm64" 18 | ) 19 | 20 | var ( 21 | // freebsdArchSupported defines the supported target architectures on freebsd 22 | freebsdArchSupported = []Architecture{ArchAmd64, ArchArm64} 23 | ) 24 | 25 | // FreeBSD build and package the fyne app for the freebsd OS 26 | type freeBSD struct { 27 | Images []containerImage 28 | defaultContext Context 29 | } 30 | 31 | var _ platformBuilder = (*freeBSD)(nil) 32 | var _ Command = (*freeBSD)(nil) 33 | 34 | func NewFreeBSD() *freeBSD { 35 | return &freeBSD{} 36 | } 37 | 38 | func (cmd *freeBSD) Name() string { 39 | return "freebsd" 40 | } 41 | 42 | // Description returns the command description 43 | func (cmd *freeBSD) Description() string { 44 | return "Build and package a fyne application for the freebsd OS" 45 | } 46 | 47 | func (cmd *freeBSD) Run() error { 48 | return commonRun(cmd.defaultContext, cmd.Images, cmd) 49 | } 50 | 51 | // Parse parses the arguments and set the usage for the command 52 | func (cmd *freeBSD) Parse(args []string) error { 53 | commonFlags, err := newCommonFlags() 54 | if err != nil { 55 | return err 56 | } 57 | 58 | flags := &freebsdFlags{ 59 | CommonFlags: commonFlags, 60 | TargetArch: &targetArchFlag{runtime.GOARCH}, 61 | } 62 | flagSet.Var(flags.TargetArch, "arch", fmt.Sprintf(`List of target architecture to build separated by comma. Supported arch: %s`, freebsdArchSupported)) 63 | 64 | flagSet.Usage = cmd.Usage 65 | flagSet.Parse(args) 66 | 67 | err = cmd.setupContainerImages(flags, flagSet.Args()) 68 | return err 69 | } 70 | 71 | // Run runs the command 72 | func (cmd *freeBSD) Build(image containerImage) (string, error) { 73 | // 74 | // package 75 | // 76 | log.Info("[i] Packaging app...") 77 | packageName := fmt.Sprintf("%s.tar.xz", cmd.defaultContext.Name) 78 | 79 | err := prepareIcon(cmd.defaultContext, image) 80 | if err != nil { 81 | return "", err 82 | } 83 | 84 | if cmd.defaultContext.Release { 85 | // Release mode 86 | err = fyneRelease(cmd.defaultContext, image) 87 | } else { 88 | // Build mode 89 | err = fynePackage(cmd.defaultContext, image) 90 | } 91 | if err != nil { 92 | return "", fmt.Errorf("could not package the Fyne app: %v", err) 93 | } 94 | image.Run(cmd.defaultContext.Volume, options{}, []string{ 95 | "mv", 96 | volume.JoinPathContainer(cmd.defaultContext.WorkDirContainer(), packageName), 97 | volume.JoinPathContainer(cmd.defaultContext.TmpDirContainer(), image.ID(), packageName), 98 | }) 99 | 100 | // Extract the resulting executable from the tarball 101 | image.Run(cmd.defaultContext.Volume, 102 | options{WorkDir: volume.JoinPathContainer(cmd.defaultContext.BinDirContainer(), image.ID())}, 103 | []string{"tar", "-xf", 104 | volume.JoinPathContainer(cmd.defaultContext.TmpDirContainer(), image.ID(), packageName), 105 | "--strip-components=3", "usr/local/bin"}) 106 | 107 | return packageName, nil 108 | } 109 | 110 | // Usage displays the command usage 111 | func (cmd *freeBSD) Usage() { 112 | data := struct { 113 | Name string 114 | Description string 115 | }{ 116 | Name: cmd.Name(), 117 | Description: cmd.Description(), 118 | } 119 | 120 | template := ` 121 | Usage: fyne-cross {{ .Name }} [options] [package] 122 | 123 | {{ .Description }} 124 | 125 | Options: 126 | ` 127 | 128 | printUsage(template, data) 129 | flagSet.PrintDefaults() 130 | } 131 | 132 | // freebsdFlags defines the command-line flags for the freebsd command 133 | type freebsdFlags struct { 134 | *CommonFlags 135 | 136 | // TargetArch represents a list of target architecture to build on separated by comma 137 | TargetArch *targetArchFlag 138 | } 139 | 140 | // setupContainerImages returns the command context for a freebsd target 141 | func (cmd *freeBSD) setupContainerImages(flags *freebsdFlags, args []string) error { 142 | targetArch, err := targetArchFromFlag(*flags.TargetArch, freebsdArchSupported) 143 | if err != nil { 144 | return fmt.Errorf("could not make build context for %s OS: %s", freebsdOS, err) 145 | } 146 | 147 | ctx, err := makeDefaultContext(flags.CommonFlags, args) 148 | if err != nil { 149 | return err 150 | } 151 | 152 | cmd.defaultContext = ctx 153 | runner, err := newContainerEngine(ctx) 154 | if err != nil { 155 | return err 156 | } 157 | 158 | for _, arch := range targetArch { 159 | var image containerImage 160 | 161 | switch arch { 162 | case ArchAmd64: 163 | image = runner.createContainerImage(arch, freebsdOS, overrideDockerImage(flags.CommonFlags, freebsdImageAmd64)) 164 | image.SetEnv("GOARCH", "amd64") 165 | image.SetEnv("CC", "clang --sysroot=/freebsd --target=x86_64-unknown-freebsd12") 166 | image.SetEnv("CXX", "clang++ --sysroot=/freebsd --target=x86_64-unknown-freebsd12") 167 | if runtime.GOARCH == string(ArchArm64) { 168 | if v, ok := ctx.Env["CGO_LDFLAGS"]; ok { 169 | image.SetEnv("CGO_LDFLAGS", v+" -fuse-ld=lld") 170 | } else { 171 | image.SetEnv("CGO_LDFLAGS", "-fuse-ld=lld") 172 | } 173 | } 174 | case ArchArm64: 175 | image = runner.createContainerImage(arch, freebsdOS, overrideDockerImage(flags.CommonFlags, freebsdImageArm64)) 176 | image.SetEnv("GOARCH", "arm64") 177 | if v, ok := ctx.Env["CGO_LDFLAGS"]; ok { 178 | image.SetEnv("CGO_LDFLAGS", v+" -fuse-ld=lld") 179 | } else { 180 | image.SetEnv("CGO_LDFLAGS", "-fuse-ld=lld") 181 | } 182 | image.SetEnv("CC", "clang --sysroot=/freebsd --target=aarch64-unknown-freebsd12") 183 | image.SetEnv("CXX", "clang++ --sysroot=/freebsd --target=aarch64-unknown-freebsd12") 184 | } 185 | image.SetEnv("GOOS", "freebsd") 186 | 187 | cmd.Images = append(cmd.Images, image) 188 | } 189 | 190 | return nil 191 | } 192 | -------------------------------------------------------------------------------- /internal/command/windows_test.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func Test_makeWindowsContext(t *testing.T) { 11 | vol, err := mockDefaultVolume() 12 | require.Nil(t, err) 13 | 14 | engine, err := MakeEngine(autodetectEngine) 15 | if err != nil { 16 | t.Skip("engine not found", err) 17 | } 18 | 19 | type args struct { 20 | flags *windowsFlags 21 | args []string 22 | } 23 | tests := []struct { 24 | name string 25 | args args 26 | wantContext Context 27 | wantImages []containerImage 28 | wantErr bool 29 | }{ 30 | { 31 | name: "default", 32 | args: args{ 33 | flags: &windowsFlags{ 34 | CommonFlags: &CommonFlags{ 35 | AppBuild: 1, 36 | }, 37 | TargetArch: &targetArchFlag{"amd64"}, 38 | }, 39 | }, 40 | wantContext: Context{ 41 | AppBuild: "1", 42 | Volume: vol, 43 | CacheEnabled: true, 44 | StripDebug: true, 45 | Package: ".", 46 | Engine: engine, 47 | Env: map[string]string{}, 48 | }, 49 | wantImages: []containerImage{ 50 | &localContainerImage{ 51 | baseContainerImage: baseContainerImage{ 52 | arch: "amd64", 53 | os: "windows", 54 | id: "windows-amd64", 55 | env: map[string]string{"GOOS": "windows", "GOARCH": "amd64", "CC": "zig cc -target x86_64-windows-gnu -Wdeprecated-non-prototype -Wl,--subsystem,windows", "CXX": "zig c++ -target x86_64-windows-gnu -Wdeprecated-non-prototype -Wl,--subsystem,windows"}, 56 | mount: []containerMountPoint{ 57 | {"project", vol.WorkDirHost(), vol.WorkDirContainer()}, 58 | {"cache", vol.CacheDirHost(), vol.CacheDirContainer()}, 59 | }, 60 | DockerImage: windowsImage, 61 | }, 62 | }, 63 | }, 64 | }, 65 | { 66 | name: "console", 67 | args: args{ 68 | flags: &windowsFlags{ 69 | CommonFlags: &CommonFlags{ 70 | AppBuild: 1, 71 | }, 72 | TargetArch: &targetArchFlag{"386"}, 73 | Console: true, 74 | }, 75 | }, 76 | wantContext: Context{ 77 | AppBuild: "1", 78 | Volume: vol, 79 | CacheEnabled: true, 80 | StripDebug: true, 81 | Package: ".", 82 | Engine: engine, 83 | Env: map[string]string{}, 84 | }, 85 | wantImages: []containerImage{ 86 | &localContainerImage{ 87 | baseContainerImage: baseContainerImage{ 88 | arch: "386", 89 | os: "windows", 90 | id: "windows-386", 91 | env: map[string]string{"GOOS": "windows", "GOARCH": "386", "CC": "zig cc -target x86-windows-gnu -Wdeprecated-non-prototype -Wl,--subsystem,windows", "CXX": "zig c++ -target x86-windows-gnu -Wdeprecated-non-prototype -Wl,--subsystem,windows"}, 92 | mount: []containerMountPoint{ 93 | {"project", vol.WorkDirHost(), vol.WorkDirContainer()}, 94 | {"cache", vol.CacheDirHost(), vol.CacheDirContainer()}, 95 | }, 96 | DockerImage: windowsImage, 97 | }, 98 | }, 99 | }, 100 | }, 101 | { 102 | name: "custom ldflags", 103 | args: args{ 104 | flags: &windowsFlags{ 105 | CommonFlags: &CommonFlags{ 106 | AppBuild: 1, 107 | Ldflags: "-X main.version=1.2.3", 108 | }, 109 | TargetArch: &targetArchFlag{"amd64"}, 110 | }, 111 | }, 112 | wantContext: Context{ 113 | AppBuild: "1", 114 | Volume: vol, 115 | CacheEnabled: true, 116 | StripDebug: true, 117 | Package: ".", 118 | Engine: engine, 119 | Env: map[string]string{ 120 | "GOFLAGS": "-ldflags=-X -ldflags=main.version=1.2.3", 121 | }, 122 | }, 123 | wantImages: []containerImage{ 124 | &localContainerImage{ 125 | baseContainerImage: baseContainerImage{ 126 | arch: "amd64", 127 | os: "windows", 128 | id: "windows-amd64", 129 | env: map[string]string{"GOOS": "windows", "GOARCH": "amd64", "CC": "zig cc -target x86_64-windows-gnu -Wdeprecated-non-prototype -Wl,--subsystem,windows", "CXX": "zig c++ -target x86_64-windows-gnu -Wdeprecated-non-prototype -Wl,--subsystem,windows"}, 130 | mount: []containerMountPoint{ 131 | {"project", vol.WorkDirHost(), vol.WorkDirContainer()}, 132 | {"cache", vol.CacheDirHost(), vol.CacheDirContainer()}, 133 | }, 134 | DockerImage: windowsImage, 135 | }, 136 | }, 137 | }, 138 | }, 139 | { 140 | name: "custom docker image", 141 | args: args{ 142 | flags: &windowsFlags{ 143 | CommonFlags: &CommonFlags{ 144 | AppBuild: 1, 145 | DockerImage: "test", 146 | }, 147 | TargetArch: &targetArchFlag{"amd64"}, 148 | }, 149 | }, 150 | wantContext: Context{ 151 | AppBuild: "1", 152 | Volume: vol, 153 | CacheEnabled: true, 154 | StripDebug: true, 155 | Package: ".", 156 | Engine: engine, 157 | Env: map[string]string{}, 158 | }, 159 | wantImages: []containerImage{ 160 | &localContainerImage{ 161 | baseContainerImage: baseContainerImage{ 162 | arch: "amd64", 163 | os: "windows", 164 | id: "windows-amd64", 165 | env: map[string]string{"GOOS": "windows", "GOARCH": "amd64", "CC": "zig cc -target x86_64-windows-gnu -Wdeprecated-non-prototype -Wl,--subsystem,windows", "CXX": "zig c++ -target x86_64-windows-gnu -Wdeprecated-non-prototype -Wl,--subsystem,windows"}, 166 | mount: []containerMountPoint{ 167 | {"project", vol.WorkDirHost(), vol.WorkDirContainer()}, 168 | {"cache", vol.CacheDirHost(), vol.CacheDirContainer()}, 169 | }, 170 | DockerImage: "test", 171 | }, 172 | }, 173 | }, 174 | }, 175 | } 176 | for _, tt := range tests { 177 | t.Run(tt.name, func(t *testing.T) { 178 | t.Run(tt.name, func(t *testing.T) { 179 | windows := NewWindowsCommand() 180 | 181 | err := windows.setupContainerImages(tt.args.flags, tt.args.args) 182 | if tt.wantErr { 183 | require.NotNil(t, err) 184 | return 185 | } 186 | require.Nil(t, err) 187 | assert.Equal(t, tt.wantContext, windows.defaultContext) 188 | 189 | for index := range windows.Images { 190 | windows.Images[index].(*localContainerImage).runner = nil 191 | } 192 | 193 | assert.Equal(t, tt.wantImages, windows.Images) 194 | }) 195 | }) 196 | } 197 | } 198 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Fyne Cross 2 | 3 | [![CI](https://github.com/fyne-io/fyne-cross/workflows/CI/badge.svg)](https://github.com/fyne-io/fyne-cross/actions?query=workflow%3ACI) [![Go Report Card](https://goreportcard.com/badge/github.com/fyne-io/fyne-cross)](https://goreportcard.com/report/github.com/fyne-io/fyne-cross) [![GoDoc](https://godoc.org/github.com/fyne-io/fyne-cross?status.svg)](http://godoc.org/github.com/fyne-io/fyne-cross) [![version](https://img.shields.io/github/v/tag/fyne-io/fyne-cross?label=version)]() 4 | 5 | fyne-cross is a simple tool to cross compile and create distribution packages 6 | for [Fyne](https://fyne.io) applications using docker images that include Linux, 7 | the MinGW compiler for Windows, FreeBSD, and a macOS SDK, along with the Fyne 8 | requirements. 9 | 10 | Supported targets are: 11 | 12 | - darwin/amd64 13 | - darwin/arm64 14 | - freebsd/amd64 15 | - freebsd/arm64 16 | - linux/amd64 17 | - linux/386 18 | - linux/arm 19 | - linux/arm64 20 | - windows/amd64 21 | - windows/arm64 22 | - windows/386 23 | - android ([multiple architectures](https://developer.android.com/ndk/guides/abis)) 24 | - android/386 25 | - android/amd64 26 | - android/arm 27 | - android/arm64 28 | - ios 29 | 30 | > Note: 31 | > 32 | > - iOS compilation is supported only on darwin hosts. See [fyne pre-requisites](https://developer.fyne.io/started/#prerequisites) for details. 33 | > - macOS packaging for public distribution (release mode) is supported only on darwin hosts. 34 | > - windows packaging for public distribution (release mode) is supported only on windows hosts. 35 | > - starting from v1.1.0: 36 | > - cross-compile from NOT `darwin` (i.e. linux) to `darwin`: requires a copy of the macOS SDK on the host. The fyne-cross `darwin-sdk-extractor` command can be used to extract the SDK from the XCode CLI Tool file, see the [Extract the macOS SDK](#extract_macos_sdk) section below. 37 | > - cross-compile from `darwin` to `darwin` by default will use under the hood the fyne CLI tool and requires Go and the macOS SDK installed on the host. 38 | > - starting from v1.4.0, Arm64 hosts are supported for all platforms except Android. 39 | 40 | ## Requirements 41 | 42 | - go >= 1.19 43 | - docker 44 | 45 | ### Installation 46 | 47 | ```sh 48 | go install github.com/fyne-io/fyne-cross@latest 49 | ``` 50 | 51 | To install a fyne-cross with kubernetes engine support: 52 | 53 | ```sh 54 | go install -tags k8s github.com/fyne-io/fyne-cross@latest 55 | ``` 56 | 57 | > `fyne-cross` will be installed in GOPATH/bin, unless GOBIN is set. 58 | 59 | ### Updating docker images 60 | 61 | To update to a newer docker image the `--pull` flag can be specified. 62 | If set, fyne-cross will attempt to pull the image required to cross compile the application for the specified target. 63 | 64 | For example: 65 | 66 | ```sh 67 | fyne-cross linux --pull 68 | ``` 69 | 70 | will pull only the `fyne-cross:base-latest` image required to cross compile for linux target. 71 | 72 | ## Usage 73 | 74 | ```sh 75 | fyne-cross [options] 76 | 77 | The commands are: 78 | 79 | darwin Build and package a fyne application for the darwin OS 80 | linux Build and package a fyne application for the linux OS 81 | windows Build and package a fyne application for the windows OS 82 | android Build and package a fyne application for the android OS 83 | ios Build and package a fyne application for the iOS OS 84 | freebsd Build and package a fyne application for the freebsd OS 85 | version Print the fyne-cross version information 86 | 87 | Use "fyne-cross -help" for more information about a command. 88 | ``` 89 | 90 | ### Wildcards 91 | 92 | The `arch` flag support wildcards in case want to compile against all supported GOARCH for a specified GOOS 93 | 94 | Example: 95 | 96 | ```sh 97 | fyne-cross windows -arch=* 98 | ``` 99 | 100 | is equivalent to 101 | 102 | ```sh 103 | fyne-cross windows -arch=amd64,386 104 | ``` 105 | 106 | ## Example 107 | 108 | The example below cross compile and package the [fyne examples application](https://github.com/fyne-io/examples) 109 | 110 | ``` 111 | git clone https://github.com/fyne-io/examples.git 112 | cd examples 113 | ``` 114 | 115 | ### Compile and package the main example app 116 | 117 | ```sh 118 | fyne-cross linux 119 | ``` 120 | 121 | > Note: by default fyne-cross will compile the package into the current dir. 122 | > 123 | > The command above is equivalent to: `fyne-cross linux .` 124 | 125 | ### Compile and package a particular example app 126 | 127 | ```sh 128 | fyne-cross linux -output bugs ./cmd/bugs 129 | ``` 130 | 131 | ## Extract the macOS SDK for OSX/Darwin/Apple cross-compiling 132 | 133 | cross-compile from NOT `darwin` (i.e. linux) to `darwin` requires a copy of the macOS SDK on the host. 134 | 135 | The fyne-cross `darwin-sdk-extractor` command can be used to extract the SDK from the XCode CLI Tool file. 136 | 137 | **[Please ensure you have read and understood the Xcode license terms before continuing.](https://www.apple.com/legal/sla/docs/xcode.pdf)** 138 | 139 | To extract the SDKs: 140 | 141 | 1. [Download Command Line Tools for Xcode](https://developer.apple.com/download/all/?q=Command%20Line%20Tools) 12.5.1 (macOS SDK 11.3) 142 | 2. Run: `fyne-cross darwin-sdk-extract --xcode-path /path/to/Command_Line_Tools_for_Xcode_12.5.1.dmg` 143 | 144 | - Once extraction has been done, you should have a SDKs directory created. 145 | This directory should contains at least 2 SDKs (ex. `SDKs/MacOSX11.3.sdk/` and `SDKs/MacOSX10.15.sdk/`) 146 | 147 | 3. Specify explicitly which SDK you want to use in your fyne-cross command with --macosx-sdk-path: 148 | `fyne-cross darwin --macosx-sdk-path /full/path/to/SDKs/MacOSX11.3.sdk -app-id your.app.id` 149 | 150 | > Note: current version supports only MacOS SDK 11.3 151 | 152 | ## Contribute 153 | 154 | - Fork and clone the repository 155 | - Make and test your changes 156 | - Open a pull request against the `develop` branch 157 | 158 | ### Contributors 159 | 160 | See [contributors](https://github.com/fyne-io/fyne-cross/graphs/contributors) page 161 | 162 | ## Credits 163 | 164 | - [osxcross](https://github.com/tpoechtrager/osxcross) for the macOS Cross toolchain for Linux 165 | - [golang-cross](https://github.com/docker/golang-cross) for the inspiration and the docker images used in the initial versions 166 | - [xgo](https://github.com/karalabe/xgo) for the inspiration 167 | -------------------------------------------------------------------------------- /internal/command/linux.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | import ( 4 | "fmt" 5 | "runtime" 6 | 7 | "github.com/fyne-io/fyne-cross/internal/log" 8 | "github.com/fyne-io/fyne-cross/internal/volume" 9 | ) 10 | 11 | const ( 12 | // linuxOS it the linux OS name 13 | linuxOS = "linux" 14 | // linuxImage is the fyne-cross image for the Linux OS 15 | linuxImageAmd64 = "fyneio/fyne-cross-images:linux" 16 | linuxImage386 = "fyneio/fyne-cross-images:linux" 17 | linuxImageArm64 = "fyneio/fyne-cross-images:linux" 18 | linuxImageArm = "fyneio/fyne-cross-images:linux" 19 | ) 20 | 21 | var ( 22 | // linuxArchSupported defines the supported target architectures on linux 23 | linuxArchSupported = []Architecture{ArchAmd64, Arch386, ArchArm, ArchArm64} 24 | ) 25 | 26 | // linux build and package the fyne app for the linux OS 27 | type linux struct { 28 | Images []containerImage 29 | defaultContext Context 30 | } 31 | 32 | var _ platformBuilder = (*linux)(nil) 33 | var _ Command = (*linux)(nil) 34 | 35 | func NewLinuxCommand() *linux { 36 | return &linux{} 37 | } 38 | 39 | func (cmd *linux) Name() string { 40 | return "linux" 41 | } 42 | 43 | // Description returns the command description 44 | func (cmd *linux) Description() string { 45 | return "Build and package a fyne application for the linux OS" 46 | } 47 | 48 | func (cmd *linux) Run() error { 49 | return commonRun(cmd.defaultContext, cmd.Images, cmd) 50 | } 51 | 52 | // Parse parses the arguments and set the usage for the command 53 | func (cmd *linux) Parse(args []string) error { 54 | commonFlags, err := newCommonFlags() 55 | if err != nil { 56 | return err 57 | } 58 | 59 | flags := &linuxFlags{ 60 | CommonFlags: commonFlags, 61 | TargetArch: &targetArchFlag{runtime.GOARCH}, 62 | } 63 | flagSet.Var(flags.TargetArch, "arch", fmt.Sprintf(`List of target architecture to build separated by comma. Supported arch: %s`, linuxArchSupported)) 64 | 65 | flagSet.Usage = cmd.Usage 66 | flagSet.Parse(args) 67 | 68 | err = cmd.setupContainerImages(flags, flagSet.Args()) 69 | return err 70 | } 71 | 72 | // Run runs the command 73 | func (cmd *linux) Build(image containerImage) (string, error) { 74 | err := prepareIcon(cmd.defaultContext, image) 75 | if err != nil { 76 | return "", err 77 | } 78 | 79 | // 80 | // package 81 | // 82 | log.Info("[i] Packaging app...") 83 | packageName := fmt.Sprintf("%s.tar.xz", cmd.defaultContext.Name) 84 | 85 | if cmd.defaultContext.Release { 86 | // Release mode 87 | err = fyneRelease(cmd.defaultContext, image) 88 | } else { 89 | // Build mode 90 | err = fynePackage(cmd.defaultContext, image) 91 | } 92 | if err != nil { 93 | return "", fmt.Errorf("could not package the Fyne app: %v", err) 94 | } 95 | image.Run(cmd.defaultContext.Volume, options{}, []string{ 96 | "mv", 97 | volume.JoinPathContainer(cmd.defaultContext.WorkDirContainer(), packageName), 98 | volume.JoinPathContainer(cmd.defaultContext.TmpDirContainer(), image.ID(), packageName), 99 | }) 100 | 101 | // Extract the resulting executable from the tarball 102 | image.Run(cmd.defaultContext.Volume, 103 | options{WorkDir: volume.JoinPathContainer(cmd.defaultContext.BinDirContainer(), image.ID())}, 104 | []string{"tar", "-xf", 105 | volume.JoinPathContainer(cmd.defaultContext.TmpDirContainer(), image.ID(), packageName), 106 | "--strip-components=3", "usr/local/bin"}) 107 | 108 | return packageName, nil 109 | } 110 | 111 | // Usage displays the command usage 112 | func (cmd *linux) Usage() { 113 | data := struct { 114 | Name string 115 | Description string 116 | }{ 117 | Name: cmd.Name(), 118 | Description: cmd.Description(), 119 | } 120 | 121 | template := ` 122 | Usage: fyne-cross {{ .Name }} [options] [package] 123 | 124 | {{ .Description }} 125 | 126 | Options: 127 | ` 128 | 129 | printUsage(template, data) 130 | flagSet.PrintDefaults() 131 | } 132 | 133 | // linuxFlags defines the command-line flags for the linux command 134 | type linuxFlags struct { 135 | *CommonFlags 136 | 137 | // TargetArch represents a list of target architecture to build on separated by comma 138 | TargetArch *targetArchFlag 139 | } 140 | 141 | // setupContainerImages returns the command ContainerImages for a linux target 142 | func (cmd *linux) setupContainerImages(flags *linuxFlags, args []string) error { 143 | targetArch, err := targetArchFromFlag(*flags.TargetArch, linuxArchSupported) 144 | if err != nil { 145 | return fmt.Errorf("could not make build context for %s OS: %s", linuxOS, err) 146 | } 147 | 148 | ctx, err := makeDefaultContext(flags.CommonFlags, args) 149 | if err != nil { 150 | return err 151 | } 152 | 153 | cmd.defaultContext = ctx 154 | runner, err := newContainerEngine(ctx) 155 | if err != nil { 156 | return err 157 | } 158 | 159 | for _, arch := range targetArch { 160 | var image containerImage 161 | 162 | switch arch { 163 | case ArchAmd64: 164 | image = runner.createContainerImage(arch, linuxOS, overrideDockerImage(flags.CommonFlags, linuxImageAmd64)) 165 | image.SetEnv("GOARCH", "amd64") 166 | image.SetEnv("CC", "zig cc -target x86_64-linux-gnu -isystem /usr/include -L/usr/lib/x86_64-linux-gnu") 167 | image.SetEnv("CXX", "zig c++ -target x86_64-linux-gnu -isystem /usr/include -L/usr/lib/x86_64-linux-gnu") 168 | case Arch386: 169 | image = runner.createContainerImage(arch, linuxOS, overrideDockerImage(flags.CommonFlags, linuxImage386)) 170 | image.SetEnv("GOARCH", "386") 171 | image.SetEnv("CC", "zig cc -target x86-linux-gnu -isystem /usr/include -L/usr/lib/i386-linux-gnu") 172 | image.SetEnv("CXX", "zig c++ -target x86-linux-gnu -isystem /usr/include -L/usr/lib/i386-linux-gnu") 173 | case ArchArm: 174 | image = runner.createContainerImage(arch, linuxOS, overrideDockerImage(flags.CommonFlags, linuxImageArm)) 175 | image.SetEnv("GOARCH", "arm") 176 | image.SetEnv("GOARM", "7") 177 | image.SetEnv("CC", "zig cc -target arm-linux-gnueabihf -isystem /usr/include -L/usr/lib/arm-linux-gnueabihf") 178 | image.SetEnv("CXX", "zig c++ -target arm-linux-gnueabihf -isystem /usr/include -L/usr/lib/arm-linux-gnueabihf") 179 | case ArchArm64: 180 | image = runner.createContainerImage(arch, linuxOS, overrideDockerImage(flags.CommonFlags, linuxImageArm64)) 181 | image.SetEnv("GOARCH", "arm64") 182 | image.SetEnv("CC", "zig cc -target aarch64-linux-gnu -isystem /usr/include -L/usr/lib/aarch64-linux-gnu") 183 | image.SetEnv("CXX", "zig c++ -target aarch64-linux-gnu -isystem /usr/include -L/usr/lib/aarch64-linux-gnu") 184 | } 185 | 186 | image.SetEnv("GOOS", "linux") 187 | 188 | cmd.Images = append(cmd.Images, image) 189 | } 190 | 191 | return nil 192 | } 193 | -------------------------------------------------------------------------------- /internal/cloud/kubernetes.go: -------------------------------------------------------------------------------- 1 | package cloud 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | "path/filepath" 8 | "time" 9 | 10 | core "k8s.io/api/core/v1" 11 | "k8s.io/apimachinery/pkg/api/resource" 12 | meta "k8s.io/apimachinery/pkg/apis/meta/v1" 13 | "k8s.io/apimachinery/pkg/util/wait" 14 | "k8s.io/client-go/kubernetes" 15 | "k8s.io/client-go/rest" 16 | "k8s.io/client-go/tools/clientcmd" 17 | "k8s.io/client-go/tools/remotecommand" 18 | "k8s.io/client-go/util/homedir" 19 | "k8s.io/kubectl/pkg/scheme" 20 | ) 21 | 22 | type Mount struct { 23 | Name string 24 | PathInContainer string 25 | } 26 | 27 | type Env struct { 28 | Name string 29 | Value string 30 | } 31 | 32 | type K8sClient struct { 33 | config *rest.Config 34 | kubectl *kubernetes.Clientset 35 | } 36 | 37 | type Pod struct { 38 | client *K8sClient 39 | pod *core.Pod 40 | name string 41 | namespace string 42 | workDir string 43 | } 44 | 45 | var Log func(string, ...interface{}) 46 | 47 | func GetKubernetesClient() (*K8sClient, error) { 48 | kubeconfig := filepath.Join(homedir.HomeDir(), ".kube", "config") 49 | 50 | var config *rest.Config 51 | var err error 52 | 53 | if !Exists(kubeconfig) { 54 | // No configuration file, try probing in cluster config 55 | config, err = rest.InClusterConfig() 56 | if err != nil { 57 | return nil, err 58 | } 59 | } else { 60 | // Try to build cluster configuration from file 61 | config, err = clientcmd.BuildConfigFromFlags("", kubeconfig) 62 | if err != nil { 63 | return nil, err 64 | } 65 | } 66 | 67 | kubectl, err := kubernetes.NewForConfig(config) 68 | if err != nil { 69 | return nil, err 70 | } 71 | 72 | return &K8sClient{config: config, kubectl: kubectl}, nil 73 | } 74 | 75 | func (k8s *K8sClient) NewPod(ctx context.Context, name string, image string, namespace string, storageLimit string, workDir string, mount []Mount, environs []Env) (*Pod, error) { 76 | var volumesMount []core.VolumeMount 77 | var volumes []core.Volume 78 | 79 | quantity := resource.MustParse(storageLimit) 80 | 81 | for _, mountPoint := range mount { 82 | volumesMount = append(volumesMount, core.VolumeMount{ 83 | Name: mountPoint.Name, 84 | MountPath: mountPoint.PathInContainer, 85 | }) 86 | volumes = append(volumes, core.Volume{ 87 | Name: mountPoint.Name, 88 | VolumeSource: core.VolumeSource{EmptyDir: &core.EmptyDirVolumeSource{ 89 | SizeLimit: &quantity, 90 | }}, 91 | }) 92 | } 93 | 94 | var envVar []core.EnvVar 95 | 96 | for _, env := range environs { 97 | envVar = append(envVar, core.EnvVar{ 98 | Name: env.Name, 99 | Value: env.Value, 100 | }) 101 | } 102 | 103 | pod := &core.Pod{ 104 | ObjectMeta: meta.ObjectMeta{Name: name, 105 | Labels: map[string]string{ 106 | "app": "fyne-cross", 107 | }}, 108 | Spec: core.PodSpec{ 109 | RestartPolicy: core.RestartPolicyNever, 110 | Containers: []core.Container{ 111 | { 112 | Name: "fyne-cross", 113 | Image: image, 114 | ImagePullPolicy: core.PullAlways, 115 | Command: []string{"/bin/bash"}, 116 | // The pod will stop itself after 30min 117 | Args: []string{"-c", "trap : TERM INT; sleep 1800 & wait"}, 118 | Env: envVar, 119 | VolumeMounts: volumesMount, 120 | WorkingDir: workDir, 121 | }, 122 | }, 123 | Volumes: volumes, 124 | Affinity: &core.Affinity{ 125 | PodAntiAffinity: &core.PodAntiAffinity{ 126 | RequiredDuringSchedulingIgnoredDuringExecution: []core.PodAffinityTerm{ 127 | { 128 | LabelSelector: &meta.LabelSelector{ 129 | MatchExpressions: []meta.LabelSelectorRequirement{ 130 | { 131 | Key: "app", 132 | Operator: "In", 133 | Values: []string{"fyne-cross"}, 134 | }, 135 | }, 136 | }, 137 | TopologyKey: "kubernetes.io/hostname", 138 | }, 139 | }, 140 | }, 141 | }, 142 | }, 143 | } 144 | 145 | logWrapper("Creating pod %q: %v", name, pod) 146 | 147 | // Start pod 148 | api := k8s.kubectl.CoreV1() 149 | 150 | instance, err := api.Pods(namespace).Create( 151 | ctx, pod, meta.CreateOptions{}, 152 | ) 153 | if err != nil { 154 | return nil, err 155 | } 156 | 157 | logWrapper("Waiting for pod to be ready") 158 | err = wait.PollUntilContextTimeout(ctx, time.Second, time.Duration(10)*time.Minute, true, func(ctx context.Context) (bool, error) { 159 | pod, err := api.Pods(namespace).Get(ctx, name, meta.GetOptions{}) 160 | if err != nil { 161 | return false, err 162 | } 163 | 164 | switch pod.Status.Phase { 165 | case core.PodRunning: 166 | return true, nil 167 | case core.PodFailed, core.PodSucceeded: 168 | return false, fmt.Errorf("pod terminated") 169 | } 170 | return false, nil 171 | }) 172 | if err != nil { 173 | return nil, err 174 | } 175 | 176 | return &Pod{ 177 | client: k8s, 178 | pod: instance, 179 | name: name, 180 | namespace: namespace, 181 | workDir: workDir, 182 | }, nil 183 | } 184 | 185 | func (p *Pod) Run(ctx context.Context, workDir string, cmdArgs []string) error { 186 | api := p.client.kubectl.CoreV1() 187 | 188 | if workDir != "" && workDir != p.workDir { 189 | shellCommand := "cd " + workDir + " && " + cmdArgs[0] + " " 190 | 191 | for _, s := range cmdArgs[1:] { 192 | shellCommand = shellCommand + fmt.Sprintf("%q ", s) 193 | } 194 | 195 | cmdArgs = []string{ 196 | "sh", 197 | "-c", 198 | shellCommand, 199 | } 200 | } 201 | 202 | req := api.RESTClient().Post().Resource("pods").Name(p.name). 203 | Namespace(p.namespace).SubResource("exec") 204 | option := &core.PodExecOptions{ 205 | Command: cmdArgs, 206 | Stdin: true, 207 | Stdout: true, 208 | Stderr: true, 209 | TTY: false, 210 | } 211 | req.VersionedParams( 212 | option, 213 | scheme.ParameterCodec, 214 | ) 215 | exec, err := remotecommand.NewSPDYExecutor(p.client.config, "POST", req.URL()) 216 | if err != nil { 217 | return err 218 | } 219 | 220 | logWrapper("Executing command %v", cmdArgs) 221 | err = exec.StreamWithContext(ctx, remotecommand.StreamOptions{ 222 | Stdin: os.Stdin, 223 | Stdout: os.Stderr, 224 | Stderr: os.Stderr, 225 | Tty: false, 226 | }) 227 | return err 228 | } 229 | 230 | func (p *Pod) Close() error { 231 | deletePolicy := meta.DeletePropagationForeground 232 | if err := p.client.kubectl.CoreV1().Pods(p.namespace).Delete(context.Background(), p.name, meta.DeleteOptions{ 233 | PropagationPolicy: &deletePolicy}); err != nil { 234 | return err 235 | } 236 | return nil 237 | } 238 | 239 | func logWrapper(format string, p ...interface{}) { 240 | if Log == nil { 241 | return 242 | } 243 | Log(format, p...) 244 | } 245 | -------------------------------------------------------------------------------- /internal/volume/volume.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package volume implements the docker host-container volume mounting 3 | */ 4 | package volume 5 | 6 | import ( 7 | "archive/zip" 8 | "fmt" 9 | "io" 10 | "os" 11 | "path" 12 | "path/filepath" 13 | "strings" 14 | ) 15 | 16 | const ( 17 | fyneCrossPrefix = "fyne-cross" 18 | 19 | binRelativePath = "fyne-cross/bin" 20 | distRelativePath = "fyne-cross/dist" 21 | tmpRelativePath = "fyne-cross/tmp" 22 | 23 | binDirContainer = "/app/" + binRelativePath 24 | cacheDirContainer = "/go" 25 | goCacheDirContainer = "/go/go-build" 26 | distDirContainer = "/app/" + distRelativePath 27 | tmpDirContainer = "/app/" + tmpRelativePath 28 | workDirContainer = "/app" 29 | ) 30 | 31 | // Copy copies a resource from src to dest 32 | func Copy(src string, dst string) error { 33 | data, err := os.ReadFile(src) 34 | if err != nil { 35 | return err 36 | } 37 | return os.WriteFile(dst, data, 0644) 38 | } 39 | 40 | // DefaultCacheDirHost returns the default cache dir on the host 41 | func DefaultCacheDirHost() (string, error) { 42 | userCacheDir, err := os.UserCacheDir() 43 | if err != nil { 44 | return "", fmt.Errorf("cannot get the path for the system cache directory on the host %s", err) 45 | } 46 | return JoinPathHost(userCacheDir, fyneCrossPrefix), nil 47 | } 48 | 49 | // DefaultWorkDirHost returns the default work dir on the host 50 | func DefaultWorkDirHost() (string, error) { 51 | wd, err := os.Getwd() 52 | if err != nil { 53 | return "", fmt.Errorf("cannot get the path for the work directory on the host %s", err) 54 | } 55 | return wd, nil 56 | } 57 | 58 | // Mount mounts the host folder into the container. 59 | func Mount(workDirHost string, cacheDirHost string) (Volume, error) { 60 | var err error 61 | 62 | if workDirHost == "" { 63 | workDirHost, err = DefaultWorkDirHost() 64 | if err != nil { 65 | return Volume{}, err 66 | } 67 | } 68 | 69 | if cacheDirHost == "" { 70 | cacheDirHost, err = DefaultCacheDirHost() 71 | if err != nil { 72 | return Volume{}, err 73 | } 74 | } 75 | 76 | l := Volume{ 77 | binDirHost: JoinPathHost(workDirHost, binRelativePath), 78 | cacheDirHost: cacheDirHost, 79 | distDirHost: JoinPathHost(workDirHost, distRelativePath), 80 | tmpDirHost: JoinPathHost(workDirHost, tmpRelativePath), 81 | workDirHost: workDirHost, 82 | } 83 | 84 | err = createHostDirs(l) 85 | if err != nil { 86 | return l, err 87 | } 88 | 89 | return l, nil 90 | } 91 | 92 | // JoinPathContainer joins any number of path elements into a single path, 93 | // separating them with the Container OS specific Separator. 94 | func JoinPathContainer(elem ...string) string { 95 | return path.Clean(strings.Join(elem, "/")) 96 | } 97 | 98 | // JoinPathHost joins any number of path elements into a single path, 99 | // separating them with the Host OS specific Separator. 100 | func JoinPathHost(elem ...string) string { 101 | return filepath.Clean(filepath.Join(elem...)) 102 | } 103 | 104 | // Zip compress the source file into a zip archive 105 | func Zip(source string, archive string) error { 106 | 107 | sourceData, err := os.Open(source) 108 | if err != nil { 109 | return fmt.Errorf("could not read the source file content: %s", err) 110 | } 111 | defer sourceData.Close() 112 | 113 | // Get the file information 114 | sourceInfo, err := sourceData.Stat() 115 | if err != nil { 116 | return fmt.Errorf("could not get the source file info: %s", err) 117 | } 118 | 119 | // Create the archive file 120 | archiveFile, err := os.Create(archive) 121 | if err != nil { 122 | return fmt.Errorf("could not create the zip archive file: %s", err) 123 | } 124 | 125 | // Create a new zip archive. 126 | zipWriter := zip.NewWriter(archiveFile) 127 | 128 | header, err := zip.FileInfoHeader(sourceInfo) 129 | if err != nil { 130 | return fmt.Errorf("could not create the file header: %s", err) 131 | } 132 | header.Method = zip.Deflate 133 | 134 | w, err := zipWriter.CreateHeader(header) 135 | if err != nil { 136 | return fmt.Errorf("could not add the source file to the zip archive: %s", err) 137 | } 138 | 139 | _, err = io.Copy(w, sourceData) 140 | if err != nil { 141 | return fmt.Errorf("could not write the source file content into the zip archive: %s", err) 142 | } 143 | 144 | // Make sure to check the error on Close. 145 | err = zipWriter.Close() 146 | if err != nil { 147 | return fmt.Errorf("could not close the zip archive: %s", err) 148 | } 149 | 150 | return archiveFile.Close() 151 | } 152 | 153 | // Volume represents the fyne-cross projec layout 154 | type Volume struct { 155 | binDirHost string 156 | cacheDirHost string 157 | distDirHost string 158 | tmpDirHost string 159 | workDirHost string 160 | } 161 | 162 | // BinDirContainer returns the fyne-cross bin dir on the container 163 | func (l Volume) BinDirContainer() string { 164 | return binDirContainer 165 | } 166 | 167 | // BinDirHost returns the fyne-cross bin dir on the host 168 | func (l Volume) BinDirHost() string { 169 | return l.binDirHost 170 | } 171 | 172 | // CacheDirContainer returns the fyne-cross cache dir on the container 173 | func (l Volume) CacheDirContainer() string { 174 | return cacheDirContainer 175 | } 176 | 177 | // CacheDirHost returns the fyne-cross cache dir on the host 178 | func (l Volume) CacheDirHost() string { 179 | return l.cacheDirHost 180 | } 181 | 182 | // DistDirContainer returns the fyne-cross distribution dir on the container 183 | func (l Volume) DistDirContainer() string { 184 | return distDirContainer 185 | } 186 | 187 | // DistDirHost returns the fyne-cross distribution dir on the host 188 | func (l Volume) DistDirHost() string { 189 | return l.distDirHost 190 | } 191 | 192 | // GoCacheDirContainer returns the fyne-cross Go cache dir on the container 193 | func (l Volume) GoCacheDirContainer() string { 194 | return goCacheDirContainer 195 | } 196 | 197 | // TmpDirContainer returns the fyne-cross temporary dir on the container 198 | func (l Volume) TmpDirContainer() string { 199 | return tmpDirContainer 200 | } 201 | 202 | // TmpDirHost returns the fyne-cross temporary dir on the host 203 | func (l Volume) TmpDirHost() string { 204 | return l.tmpDirHost 205 | } 206 | 207 | // WorkDirContainer returns the working dir on the host 208 | func (l Volume) WorkDirContainer() string { 209 | return workDirContainer 210 | } 211 | 212 | // WorkDirHost returns the working dir on the host 213 | func (l Volume) WorkDirHost() string { 214 | return l.workDirHost 215 | } 216 | 217 | // createHostDirs creates the fyne-cross dirs on the host, if not exists 218 | func createHostDirs(l Volume) error { 219 | dirs := []string{ 220 | l.binDirHost, 221 | l.cacheDirHost, 222 | l.distDirHost, 223 | l.tmpDirHost, 224 | } 225 | 226 | for _, dir := range dirs { 227 | err := os.MkdirAll(dir, 0755) 228 | if err != nil { 229 | return fmt.Errorf("cannot create the fyne-cross directory %s: %s", dir, err) 230 | } 231 | } 232 | return nil 233 | } 234 | -------------------------------------------------------------------------------- /internal/command/android.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path" 7 | 8 | "github.com/fyne-io/fyne-cross/internal/log" 9 | "github.com/fyne-io/fyne-cross/internal/volume" 10 | ) 11 | 12 | const ( 13 | // androidOS is the android OS name 14 | androidOS = "android" 15 | // androidImage is the fyne-cross image for the Android OS 16 | androidImage = "fyneio/fyne-cross-images:android" 17 | ) 18 | 19 | var ( 20 | // androidArchSupported defines the supported target architectures for the android OS 21 | androidArchSupported = []Architecture{ArchMultiple, ArchAmd64, Arch386, ArchArm, ArchArm64} 22 | ) 23 | 24 | // Android build and package the fyne app for the android OS 25 | type android struct { 26 | Images []containerImage 27 | defaultContext Context 28 | } 29 | 30 | var _ platformBuilder = (*android)(nil) 31 | var _ Command = (*android)(nil) 32 | 33 | func NewAndroidCommand() *android { 34 | return &android{} 35 | } 36 | 37 | func (cmd *android) Name() string { 38 | return "android" 39 | } 40 | 41 | // Description returns the command description 42 | func (cmd *android) Description() string { 43 | return "Build and package a fyne application for the android OS" 44 | } 45 | 46 | func (cmd *android) Run() error { 47 | return commonRun(cmd.defaultContext, cmd.Images, cmd) 48 | } 49 | 50 | // Parse parses the arguments and set the usage for the command 51 | func (cmd *android) Parse(args []string) error { 52 | commonFlags, err := newCommonFlags() 53 | if err != nil { 54 | return err 55 | } 56 | 57 | flags := &androidFlags{ 58 | CommonFlags: commonFlags, 59 | TargetArch: &targetArchFlag{string(ArchMultiple)}, 60 | } 61 | 62 | flagSet.Var(flags.TargetArch, "arch", fmt.Sprintf(`List of target architecture to build separated by comma. Supported arch: %s.`, androidArchSupported)) 63 | flagSet.StringVar(&flags.Keystore, "keystore", "", "The location of .keystore file containing signing information") 64 | flagSet.StringVar(&flags.KeystorePass, "keystore-pass", "", "Password for the .keystore file") 65 | flagSet.StringVar(&flags.KeyPass, "key-pass", "", "Password for the signer's private key, which is needed if the private key is password-protected") 66 | flagSet.StringVar(&flags.KeyName, "key-name", "", "Name of the key to use for signing") 67 | 68 | flagSet.Usage = cmd.Usage 69 | flagSet.Parse(args) 70 | 71 | err = cmd.setupContainerImages(flags, flagSet.Args()) 72 | return err 73 | } 74 | 75 | // Run runs the command 76 | func (cmd *android) Build(image containerImage) (string, error) { 77 | // 78 | // package 79 | // 80 | log.Info("[i] Packaging app...") 81 | 82 | packageName := fmt.Sprintf("%s.apk", cmd.defaultContext.Name) 83 | pattern := "*.apk" 84 | 85 | err := prepareIcon(cmd.defaultContext, image) 86 | if err != nil { 87 | return "", err 88 | } 89 | 90 | if cmd.defaultContext.Release { 91 | err = fyneRelease(cmd.defaultContext, image) 92 | packageName = fmt.Sprintf("%s.aab", cmd.defaultContext.Name) 93 | pattern = "*.aab" 94 | } else { 95 | err = fynePackage(cmd.defaultContext, image) 96 | } 97 | if err != nil { 98 | return "", fmt.Errorf("could not package the Fyne app: %v", err) 99 | } 100 | 101 | // move the dist package into the "dist" folder 102 | // The fyne tool sanitizes the package name to be acceptable as a 103 | // android package name. For details, see: 104 | // https://github.com/fyne-io/fyne/blob/v1.4.0/cmd/fyne/internal/mobile/build_androidapp.go#L297 105 | // To avoid to duplicate the fyne tool sanitize logic here, the location of 106 | // the dist package to move will be detected using a matching pattern 107 | command := fmt.Sprintf("mv %q/%s %q", 108 | volume.JoinPathContainer(cmd.defaultContext.WorkDirContainer(), cmd.defaultContext.Package), 109 | pattern, 110 | volume.JoinPathContainer(cmd.defaultContext.TmpDirContainer(), image.ID(), packageName), 111 | ) 112 | 113 | // move the dist package into the expected tmp/$ID/packageName location in the container 114 | // We use the shell to do the globbing and copy the file 115 | err = image.Run(cmd.defaultContext.Volume, options{}, []string{ 116 | "sh", "-c", command, 117 | }) 118 | 119 | if err != nil { 120 | return "", fmt.Errorf("could not retrieve the packaged apk") 121 | } 122 | 123 | return packageName, nil 124 | } 125 | 126 | // Usage displays the command usage 127 | func (cmd *android) Usage() { 128 | data := struct { 129 | Name string 130 | Description string 131 | }{ 132 | Name: cmd.Name(), 133 | Description: cmd.Description(), 134 | } 135 | 136 | template := ` 137 | Usage: fyne-cross {{ .Name }} [options] [package] 138 | 139 | {{ .Description }} 140 | 141 | Options: 142 | ` 143 | 144 | printUsage(template, data) 145 | flagSet.PrintDefaults() 146 | } 147 | 148 | // androidFlags defines the command-line flags for the android command 149 | type androidFlags struct { 150 | *CommonFlags 151 | 152 | Keystore string //Keystore represents the location of .keystore file containing signing information 153 | KeystorePass string //Password for the .keystore file 154 | KeyPass string //Password for the signer's private key, which is needed if the private key is password-protected 155 | KeyName string //Name of the key to use for signing 156 | 157 | // TargetArch represents a list of target architecture to build on separated by comma 158 | TargetArch *targetArchFlag 159 | } 160 | 161 | // setupContainerImages returns the command context for an android target 162 | func (cmd *android) setupContainerImages(flags *androidFlags, args []string) error { 163 | 164 | targetArch, err := targetArchFromFlag(*flags.TargetArch, androidArchSupported) 165 | if err != nil { 166 | return fmt.Errorf("could not make build context for %s OS: %s", androidOS, err) 167 | } 168 | 169 | ctx, err := makeDefaultContext(flags.CommonFlags, args) 170 | if err != nil { 171 | return err 172 | } 173 | 174 | // appID is mandatory for android 175 | if ctx.AppID == "" { 176 | return fmt.Errorf("appID is mandatory for %s", androidOS) 177 | } 178 | 179 | cmd.defaultContext = ctx 180 | runner, err := newContainerEngine(ctx) 181 | if err != nil { 182 | return err 183 | } 184 | 185 | for _, arch := range targetArch { 186 | // By default, the fyne cli tool builds a fat APK for all supported 187 | // instruction sets (arm, 386, amd64, arm64). A subset of instruction sets can 188 | // be selected by specifying target type with the architecture name. 189 | // E.g.: -os=android/arm 190 | image := runner.createContainerImage(arch, androidOS, overrideDockerImage(flags.CommonFlags, androidImage)) 191 | 192 | if path.IsAbs(flags.Keystore) { 193 | return fmt.Errorf("keystore location must be relative to the project root: %s", ctx.Volume.WorkDirHost()) 194 | } 195 | 196 | if !ctx.NoProjectUpload { 197 | if _, err := os.Stat(volume.JoinPathHost(ctx.Volume.WorkDirHost(), flags.Keystore)); err != nil { 198 | return fmt.Errorf("keystore location must be under the project root: %s", ctx.Volume.WorkDirHost()) 199 | } 200 | } 201 | 202 | if flags.Keystore != "" { 203 | cmd.defaultContext.Keystore = volume.JoinPathContainer(cmd.defaultContext.Volume.WorkDirContainer(), flags.Keystore) 204 | } 205 | cmd.defaultContext.KeystorePass = flags.KeystorePass 206 | cmd.defaultContext.KeyPass = flags.KeyPass 207 | cmd.defaultContext.KeyName = flags.KeyName 208 | 209 | cmd.Images = append(cmd.Images, image) 210 | } 211 | 212 | return nil 213 | } 214 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: "CI" 2 | on: 3 | push: 4 | branches: 5 | - master 6 | - develop 7 | pull_request: 8 | workflow_dispatch: 9 | 10 | jobs: 11 | lint: 12 | name: Lint 13 | runs-on: "ubuntu-latest" 14 | steps: 15 | - name: Setup Go environment 16 | uses: actions/setup-go@v3 17 | with: 18 | go-version: "1.23.x" 19 | 20 | - name: Install staticcheck 21 | run: go install honnef.co/go/tools/cmd/staticcheck@v0.5.1 22 | - name: Install goimports 23 | run: go install golang.org/x/tools/cmd/goimports@latest 24 | 25 | # Checks-out the repository under $GITHUB_WORKSPACE 26 | - uses: actions/checkout@v3 27 | 28 | # Run linters 29 | - name: Run go vet 30 | run: go vet ./... 31 | - name: Run goimports 32 | run: test -z $(find . -name '*.go' -type f | xargs goimports -e -d | tee /dev/stderr) 33 | - name: Run staticcheck 34 | run: staticcheck ./... 35 | 36 | test: 37 | name: "Test" 38 | runs-on: ${{ matrix.os }} 39 | strategy: 40 | matrix: 41 | os: [ubuntu-latest, macos-latest, windows-latest] 42 | # use max/min supported Go versions 43 | go-version: ["1.23.x", "1.19.x"] 44 | 45 | steps: 46 | - name: Setup Go environment 47 | id: setup-go 48 | uses: actions/setup-go@v3 49 | with: 50 | go-version: ${{ matrix.go-version }} 51 | 52 | # Checks-out the repository under $GITHUB_WORKSPACE 53 | - uses: actions/checkout@v3 54 | 55 | # Run tests 56 | - run: go test -v -cover -race ./... 57 | 58 | k8s: 59 | name: "Verify k8s build" 60 | runs-on: ${{ matrix.os }} 61 | strategy: 62 | matrix: 63 | os: [ubuntu-latest, macos-latest, windows-latest] 64 | # use max/min supported Go versions 65 | go-version: ["1.19.x"] 66 | 67 | steps: 68 | - name: Setup Go environment 69 | id: setup-go 70 | uses: actions/setup-go@v3 71 | with: 72 | go-version: ${{ matrix.go-version }} 73 | 74 | # Checks-out the repository under $GITHUB_WORKSPACE 75 | - uses: actions/checkout@v3 76 | 77 | # Run tests 78 | - run: go build -tags k8s 79 | 80 | build: 81 | name: "Build Calculator (${{ matrix.target.os }}, ${{ matrix.go-version }})" 82 | runs-on: ${{ matrix.target.host || 'ubuntu-latest' }} 83 | env: 84 | GO111MODULE: on 85 | strategy: 86 | fail-fast: false 87 | matrix: 88 | # use max/min supported Go versions 89 | go-version: ["1.23.x", "1.19.x"] 90 | target: 91 | - os: linux 92 | - os: windows 93 | ext: .exe 94 | - os: freebsd 95 | - os: android 96 | args: -app-id calc.sha${{ github.sha }} 97 | - os: darwin 98 | args: -app-id calc.sha${{ github.sha }} 99 | # Only macos-13 is supported as macos-14 is running in a VM on Mac M1 which are incompatible with docker/podman 100 | host: macos-13 101 | - os: web 102 | 103 | ## Currently not easily supported from GitHub actions. 104 | ## https://github.com/fyne-io/fyne-cross/pull/104#issuecomment-1099494308 105 | # - os: ios 106 | # args: -app-id calc.sha${{ github.sha }} 107 | # host: macos-latest 108 | 109 | steps: 110 | - name: Setup Go environment 111 | id: setup-go 112 | uses: actions/setup-go@v3 113 | with: 114 | go-version: ${{ matrix.go-version }} 115 | 116 | - name: Checkout code 117 | uses: actions/checkout@v3 118 | with: 119 | path: fyne-cross 120 | 121 | - name: Checkout fyne-io/calculator 122 | uses: actions/checkout@v3 123 | with: 124 | repository: fyne-io/calculator 125 | path: calculator 126 | 127 | - name: Cache build artifacts 128 | uses: actions/cache@v3 129 | with: 130 | path: | 131 | ~/go/pkg/mod 132 | ~/.cache/go-build 133 | ~/.cache/fyne-cross 134 | key: ${{ runner.os }}-build-cache-${{ hashFiles('**/go.sum') }} 135 | 136 | - name: Install Fyne-cross 137 | working-directory: fyne-cross 138 | run: go install 139 | 140 | - name: Install Fyne 141 | run: | 142 | go install fyne.io/fyne/v2/cmd/fyne@latest 143 | 144 | - name: Install Docker 145 | if: ${{ runner.os == 'macos' }} 146 | uses: douglascamata/setup-docker-macos-action@v1-alpha 147 | 148 | - name: Build 149 | working-directory: calculator 150 | run: | 151 | fyne-cross \ 152 | ${{ matrix.target.os }} \ 153 | ${{ matrix.target.args }} \ 154 | -debug -no-cache \ 155 | -name calculator${{ matrix.target.ext }} 156 | 157 | build-fyneterm: 158 | name: "Build Fyneterm (${{ matrix.target.os }}, ${{ matrix.go-version }})" 159 | runs-on: ${{ matrix.target.host || 'ubuntu-latest' }} 160 | env: 161 | GO111MODULE: on 162 | strategy: 163 | fail-fast: false 164 | matrix: 165 | # use max/min supported Go versions 166 | go-version: ["1.23.x", "1.19.x"] 167 | target: 168 | - os: linux 169 | - os: windows 170 | ext: .exe 171 | - os: freebsd 172 | - os: android 173 | args: -app-id calc.sha${{ github.sha }} 174 | - os: darwin 175 | args: -app-id calc.sha${{ github.sha }} 176 | # Only macos-13 is supported as macos-14 is running in a VM on Mac M1 which are incompatible with docker/podman 177 | host: macos-13 178 | 179 | ## Currently not easily supported from GitHub actions. 180 | ## https://github.com/fyne-io/fyne-cross/pull/104#issuecomment-1099494308 181 | # - os: ios 182 | # args: -app-id calc.sha${{ github.sha }} 183 | # host: macos-latest 184 | 185 | steps: 186 | - name: Setup Go environment 187 | id: setup-go 188 | uses: actions/setup-go@v3 189 | with: 190 | go-version: ${{ matrix.go-version }} 191 | 192 | - name: Checkout code 193 | uses: actions/checkout@v3 194 | with: 195 | path: fyne-cross 196 | 197 | - name: Checkout fyne-io/terminal 198 | uses: actions/checkout@v3 199 | with: 200 | repository: fyne-io/terminal 201 | path: terminal 202 | 203 | - name: Cache build artifacts 204 | uses: actions/cache@v3 205 | with: 206 | path: | 207 | ~/go/pkg/mod 208 | ~/.cache/go-build 209 | ~/.cache/fyne-cross 210 | key: ${{ runner.os }}-build-cache-${{ hashFiles('**/go.sum') }} 211 | 212 | - name: Install Fyne-cross 213 | working-directory: fyne-cross 214 | run: go install 215 | 216 | - name: Install Fyne 217 | run: | 218 | go install fyne.io/fyne/v2/cmd/fyne@latest 219 | 220 | - name: Install Docker 221 | if: ${{ runner.os == 'macos' }} 222 | uses: douglascamata/setup-docker-macos-action@v1-alpha 223 | 224 | - name: Build 225 | working-directory: terminal 226 | run: | 227 | fyne-cross \ 228 | ${{ matrix.target.os }} \ 229 | ${{ matrix.target.args }} \ 230 | -debug \ 231 | -name fyneterm${{ matrix.target.ext }} cmd/fyneterm 232 | -------------------------------------------------------------------------------- /internal/command/docker.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "os" 7 | "os/user" 8 | "path/filepath" 9 | "runtime" 10 | "strings" 11 | 12 | "github.com/fyne-io/fyne-cross/internal/log" 13 | "github.com/fyne-io/fyne-cross/internal/volume" 14 | 15 | "golang.org/x/sys/execabs" 16 | ) 17 | 18 | // Options define the options for the docker run command 19 | type options struct { 20 | WorkDir string // WorkDir set the workdir, default to volume's workdir 21 | } 22 | 23 | type localContainerEngine struct { 24 | baseEngine 25 | 26 | engine *Engine 27 | 28 | pull bool 29 | cacheEnabled bool 30 | noNetwork bool 31 | } 32 | 33 | func newLocalContainerEngine(context Context) (containerEngine, error) { 34 | return &localContainerEngine{ 35 | baseEngine: baseEngine{ 36 | env: context.Env, 37 | tags: context.Tags, 38 | vol: context.Volume, 39 | }, 40 | engine: &context.Engine, 41 | pull: context.Pull, 42 | cacheEnabled: context.CacheEnabled, 43 | noNetwork: context.NoNetwork, 44 | }, nil 45 | } 46 | 47 | type localContainerImage struct { 48 | baseContainerImage 49 | 50 | runner *localContainerEngine 51 | } 52 | 53 | var _ containerEngine = (*localContainerEngine)(nil) 54 | var _ closer = (*localContainerImage)(nil) 55 | 56 | func (r *localContainerEngine) createContainerImage(arch Architecture, OS string, image string) containerImage { 57 | ret := r.createContainerImageInternal(arch, OS, image, func(base baseContainerImage) containerImage { 58 | return &localContainerImage{ 59 | baseContainerImage: base, 60 | runner: r, 61 | } 62 | }) 63 | 64 | // mount the cache dir if cache is enabled 65 | if r.cacheEnabled { 66 | ret.SetMount("cache", r.vol.CacheDirHost(), r.vol.CacheDirContainer()) 67 | } 68 | 69 | return ret 70 | } 71 | 72 | func (*localContainerImage) close() error { 73 | return nil 74 | } 75 | 76 | func AppendEnv(args []string, environs map[string]string, quoteNeeded bool) []string { 77 | for k, v := range environs { 78 | env := k + "=" + v 79 | if quoteNeeded && strings.Contains(v, "=") { 80 | // engine requires to double quote the value when it contains 81 | // the `=` char 82 | env = fmt.Sprintf("%s=%q", k, v) 83 | } 84 | args = append(args, "-e", env) 85 | } 86 | return args 87 | } 88 | 89 | func (i *localContainerImage) Engine() containerEngine { 90 | return i.runner 91 | } 92 | 93 | // Cmd returns a command to run in a new container for the specified image 94 | func (i *localContainerImage) cmd(vol volume.Volume, opts options, cmdArgs []string) *execabs.Cmd { 95 | // define workdir 96 | w := vol.WorkDirContainer() 97 | if opts.WorkDir != "" { 98 | w = opts.WorkDir 99 | } 100 | 101 | args := []string{ 102 | "run", "--rm", "-t", 103 | "-w", w, // set workdir 104 | } 105 | 106 | mountFormat := "%s:%s:z" 107 | if runtime.GOOS == darwinOS { 108 | // When running on darwin with an Arm64 or Amd64, we rely on going through a VM setup that doesn't allow the :z 109 | mountFormat = "%s:%s" 110 | } 111 | 112 | for _, mountPoint := range i.mount { 113 | args = append(args, "-v", fmt.Sprintf(mountFormat, mountPoint.localHost, mountPoint.inContainer)) 114 | } 115 | 116 | arch := "amd64" 117 | if runtime.GOARCH == "arm64" && (runtime.GOOS != "darwin" || i.os != "android") { 118 | // If we are running on arm64, we should have arm64 image to avoid using emulation 119 | arch = runtime.GOARCH 120 | } 121 | 122 | // handle settings related to engine 123 | if i.runner.engine.IsPodman() { 124 | args = append(args, "--userns", "keep-id", "-e", "use_podman=1", "--arch="+arch) 125 | } else { 126 | args = append(args, "--platform", "linux/"+arch) 127 | 128 | // docker: pass current user id to handle mount permissions on linux and MacOS 129 | if runtime.GOOS != "windows" { 130 | u, err := user.Current() 131 | if err == nil { 132 | // Container runs as current host UID 133 | args = append(args, "--user", u.Uid) 134 | // Set HOME to something writable by the user 135 | args = append(args, "-e", "HOME=/tmp") 136 | } 137 | } 138 | } 139 | 140 | // detect ssh-agent socket for private repositories access 141 | if sshAuthSock := os.Getenv("SSH_AUTH_SOCK"); sshAuthSock != "" { 142 | if runtime.GOOS == "darwin" { 143 | // Podman doesn't yet support sshagent forwarding on macOS 144 | if !i.runner.engine.IsPodman() { 145 | // on macOS, the SSH_AUTH_SOCK is not available in the container directly, 146 | // but instead we need to the magic path "/run/host-services/ssh-auth.sock" 147 | args = append(args, "-v", "/run/host-services/ssh-auth.sock:/run/host-services/ssh-auth.sock") 148 | args = append(args, "-e", "SSH_AUTH_SOCK=/run/host-services/ssh-auth.sock") 149 | } 150 | } else if realSshAuthSock, err := filepath.EvalSymlinks(sshAuthSock); err == nil { 151 | args = append(args, "-v", fmt.Sprintf("%s:/tmp/ssh-agent", realSshAuthSock)) 152 | args = append(args, "-e", "SSH_AUTH_SOCK=/tmp/ssh-agent") 153 | } 154 | } 155 | 156 | // add default env variables 157 | args = append(args, 158 | "-e", "CGO_ENABLED=1", // enable CGO 159 | "-e", fmt.Sprintf("GOCACHE=%s", vol.GoCacheDirContainer()), // mount GOCACHE to reuse cache between builds 160 | ) 161 | 162 | if i.runner.noNetwork { 163 | args = append(args, "--network=none") 164 | } 165 | 166 | // add custom env variables 167 | args = AppendEnv(args, i.runner.env, i.env["GOOS"] != freebsdOS) 168 | args = AppendEnv(args, i.env, i.env["GOOS"] != freebsdOS) 169 | 170 | // specify the image to use 171 | args = append(args, i.DockerImage) 172 | 173 | // add the command to execute 174 | args = append(args, cmdArgs...) 175 | 176 | cmd := execabs.Command(i.runner.engine.Binary, args...) 177 | cmd.Stdout = os.Stdout 178 | cmd.Stderr = os.Stderr 179 | 180 | return cmd 181 | } 182 | 183 | // Run runs a command in a new container for the specified image 184 | func (i *localContainerImage) Run(vol volume.Volume, opts options, cmdArgs []string) error { 185 | cmd := i.cmd(vol, opts, cmdArgs) 186 | log.Debug(cmd) 187 | return cmd.Run() 188 | } 189 | 190 | // pullImage attempts to pull a newer version of the docker image 191 | func (i *localContainerImage) Prepare() error { 192 | if !i.runner.pull { 193 | return nil 194 | } 195 | 196 | log.Infof("[i] Checking for a newer version of the docker image: %s", i.DockerImage) 197 | 198 | buf := bytes.Buffer{} 199 | 200 | // run the command inside the container 201 | cmd := execabs.Command(i.runner.engine.Binary, "pull", i.DockerImage) 202 | cmd.Stdout = &buf 203 | cmd.Stderr = &buf 204 | 205 | log.Debug(cmd) 206 | 207 | err := cmd.Run() 208 | 209 | log.Debug(buf.String()) 210 | 211 | if err != nil { 212 | return fmt.Errorf("could not pull the docker image: %v", err) 213 | } 214 | 215 | log.Infof("[✓] Image is up to date") 216 | return nil 217 | } 218 | 219 | func (i *localContainerImage) Finalize(packageName string) error { 220 | // move the dist package into the "dist" folder 221 | srcPath := volume.JoinPathHost(i.runner.vol.TmpDirHost(), i.ID(), packageName) 222 | distFile := volume.JoinPathHost(i.runner.vol.DistDirHost(), i.ID(), packageName) 223 | 224 | // If packageName is empty, we are copying an entire directory directly in the DistDirHost directory 225 | if packageName == "" { 226 | err := os.RemoveAll(distFile) 227 | if err != nil { 228 | return err 229 | } 230 | } 231 | 232 | err := os.Rename(srcPath, distFile) 233 | if err != nil { 234 | return err 235 | } 236 | 237 | log.Infof("[✓] Package: %q", distFile) 238 | 239 | return nil 240 | } 241 | -------------------------------------------------------------------------------- /internal/command/windows.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | import ( 4 | "fmt" 5 | "runtime" 6 | "strings" 7 | 8 | "github.com/fyne-io/fyne-cross/internal/log" 9 | "github.com/fyne-io/fyne-cross/internal/volume" 10 | ) 11 | 12 | const ( 13 | // windowsOS it the windows OS name 14 | windowsOS = "windows" 15 | // windowsImage is the fyne-cross image for the Windows OS 16 | windowsImage = "fyneio/fyne-cross-images:windows" 17 | ) 18 | 19 | var ( 20 | // windowsArchSupported defines the supported target architectures on windows 21 | windowsArchSupported = []Architecture{ArchAmd64, ArchArm64, Arch386} 22 | ) 23 | 24 | // Windows build and package the fyne app for the windows OS 25 | type windows struct { 26 | Images []containerImage 27 | defaultContext Context 28 | } 29 | 30 | var _ platformBuilder = (*windows)(nil) 31 | var _ Command = (*windows)(nil) 32 | 33 | func NewWindowsCommand() *windows { 34 | return &windows{} 35 | } 36 | 37 | func (cmd *windows) Name() string { 38 | return "windows" 39 | } 40 | 41 | // Description returns the command description 42 | func (cmd *windows) Description() string { 43 | return "Build and package a fyne application for the windows OS" 44 | } 45 | 46 | func (cmd *windows) Run() error { 47 | return commonRun(cmd.defaultContext, cmd.Images, cmd) 48 | } 49 | 50 | // Parse parses the arguments and set the usage for the command 51 | func (cmd *windows) Parse(args []string) error { 52 | commonFlags, err := newCommonFlags() 53 | if err != nil { 54 | return err 55 | } 56 | 57 | flags := &windowsFlags{ 58 | CommonFlags: commonFlags, 59 | TargetArch: &targetArchFlag{runtime.GOARCH}, 60 | } 61 | 62 | flagSet.Var(flags.TargetArch, "arch", fmt.Sprintf(`List of target architecture to build separated by comma. Supported arch: %s`, windowsArchSupported)) 63 | flagSet.BoolVar(&flags.Console, "console", false, "If set writes a 'console binary' instead of 'GUI binary'") 64 | 65 | // flags used only in release mode 66 | flagSet.StringVar(&flags.Certificate, "certificate", "", "The name of the certificate to sign the build") 67 | flagSet.StringVar(&flags.Developer, "developer", "", "The developer identity for your Microsoft store account") 68 | flagSet.StringVar(&flags.Password, "password", "", "The password for the certificate used to sign the build") 69 | 70 | // Add exe extension to default output 71 | flagName := flagSet.Lookup("name") 72 | flagName.DefValue = fmt.Sprintf("%s.exe", flagName.DefValue) 73 | flagName.Value.Set(flagName.DefValue) 74 | 75 | flagSet.Usage = cmd.Usage 76 | flagSet.Parse(args) 77 | 78 | err = cmd.setupContainerImages(flags, flagSet.Args()) 79 | return err 80 | } 81 | 82 | // Run runs the command 83 | func (cmd *windows) Build(image containerImage) (string, error) { 84 | err := prepareIcon(cmd.defaultContext, image) 85 | if err != nil { 86 | return "", err 87 | } 88 | 89 | // Release mode 90 | if cmd.defaultContext.Release { 91 | if runtime.GOOS != windowsOS { 92 | return "", fmt.Errorf("windows release build is supported only on windows hosts") 93 | } 94 | 95 | packageName, err := fyneReleaseHost(cmd.defaultContext, image) 96 | if err != nil { 97 | return "", fmt.Errorf("could not package the Fyne app: %v", err) 98 | } 99 | 100 | // move the dist package into the expected tmp/$ID/packageName location in the container 101 | image.Run(cmd.defaultContext.Volume, options{}, []string{ 102 | "sh", "-c", fmt.Sprintf("mv %q/*.appx %q", 103 | cmd.defaultContext.WorkDirContainer(), 104 | volume.JoinPathContainer(cmd.defaultContext.TmpDirContainer(), image.ID(), packageName)), 105 | }) 106 | 107 | return packageName, nil 108 | } 109 | 110 | // 111 | // package 112 | // 113 | log.Info("[i] Packaging app...") 114 | packageName := cmd.defaultContext.Name + ".zip" 115 | 116 | // Build mode 117 | err = fynePackage(cmd.defaultContext, image) 118 | if err != nil { 119 | return "", err 120 | } 121 | 122 | executableName := cmd.defaultContext.Name + ".exe" 123 | if pos := strings.LastIndex(cmd.defaultContext.Name, ".exe"); pos > 0 { 124 | executableName = cmd.defaultContext.Name 125 | } 126 | 127 | // create a zip archive from the compiled binary under the "bin" folder 128 | // and place it under the tmp folder 129 | err = image.Run(cmd.defaultContext.Volume, options{}, []string{ 130 | "sh", "-c", fmt.Sprintf("cd %q && zip %q *.exe", 131 | volume.JoinPathContainer(cmd.defaultContext.WorkDirContainer(), cmd.defaultContext.Package), 132 | volume.JoinPathContainer(cmd.defaultContext.TmpDirContainer(), image.ID(), packageName)), 133 | }) 134 | if err != nil { 135 | return "", err 136 | } 137 | 138 | image.Run(cmd.defaultContext.Volume, options{}, []string{ 139 | "sh", "-c", fmt.Sprintf("mv %q/*.exe %q", 140 | volume.JoinPathContainer(cmd.defaultContext.WorkDirContainer(), cmd.defaultContext.Package), 141 | volume.JoinPathContainer(cmd.defaultContext.BinDirContainer(), image.ID(), executableName)), 142 | }) 143 | 144 | return packageName, nil 145 | } 146 | 147 | // Usage displays the command usage 148 | func (cmd *windows) Usage() { 149 | data := struct { 150 | Name string 151 | Description string 152 | }{ 153 | Name: cmd.Name(), 154 | Description: cmd.Description(), 155 | } 156 | 157 | template := ` 158 | Usage: fyne-cross {{ .Name }} [options] [package] 159 | 160 | {{ .Description }} 161 | 162 | Options: 163 | ` 164 | 165 | printUsage(template, data) 166 | flagSet.PrintDefaults() 167 | } 168 | 169 | // windowsFlags defines the command-line flags for the windows command 170 | type windowsFlags struct { 171 | *CommonFlags 172 | 173 | // TargetArch represents a list of target architecture to build on separated by comma 174 | TargetArch *targetArchFlag 175 | 176 | // Console defines if the Windows app will build as "console binary" instead of "GUI binary" 177 | Console bool 178 | 179 | //Certificate represents the name of the certificate to sign the build 180 | Certificate string 181 | //Developer represents the developer identity for your Microsoft store account 182 | Developer string 183 | //Password represents the password for the certificate used to sign the build [Windows] 184 | Password string 185 | } 186 | 187 | // setupContainerImages returns the command ContainerImages for a windows target 188 | func (cmd *windows) setupContainerImages(flags *windowsFlags, args []string) error { 189 | targetArch, err := targetArchFromFlag(*flags.TargetArch, windowsArchSupported) 190 | if err != nil { 191 | return fmt.Errorf("could not make build context for %s OS: %s", windowsOS, err) 192 | } 193 | 194 | ctx, err := makeDefaultContext(flags.CommonFlags, args) 195 | if err != nil { 196 | return err 197 | } 198 | 199 | ctx.Certificate = flags.Certificate 200 | ctx.Developer = flags.Developer 201 | ctx.Password = flags.Password 202 | 203 | cmd.defaultContext = ctx 204 | runner, err := newContainerEngine(ctx) 205 | if err != nil { 206 | return err 207 | } 208 | 209 | for _, arch := range targetArch { 210 | image := runner.createContainerImage(arch, windowsOS, overrideDockerImage(flags.CommonFlags, windowsImage)) 211 | 212 | image.SetEnv("GOOS", "windows") 213 | switch arch { 214 | case ArchAmd64: 215 | image.SetEnv("GOARCH", "amd64") 216 | image.SetEnv("CC", "zig cc -target x86_64-windows-gnu -Wdeprecated-non-prototype -Wl,--subsystem,windows") 217 | image.SetEnv("CXX", "zig c++ -target x86_64-windows-gnu -Wdeprecated-non-prototype -Wl,--subsystem,windows") 218 | case Arch386: 219 | image.SetEnv("GOARCH", "386") 220 | image.SetEnv("CC", "zig cc -target x86-windows-gnu -Wdeprecated-non-prototype -Wl,--subsystem,windows") 221 | image.SetEnv("CXX", "zig c++ -target x86-windows-gnu -Wdeprecated-non-prototype -Wl,--subsystem,windows") 222 | case ArchArm64: 223 | image.SetEnv("GOARCH", "arm64") 224 | image.SetEnv("CC", "zig cc -target aarch64-windows-gnu -Wdeprecated-non-prototype -Wl,--subsystem,windows") 225 | image.SetEnv("CXX", "zig c++ -target aarch64-windows-gnu -Wdeprecated-non-prototype -Wl,--subsystem,windows") 226 | } 227 | 228 | cmd.Images = append(cmd.Images, image) 229 | } 230 | 231 | return nil 232 | } 233 | -------------------------------------------------------------------------------- /internal/command/container.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/fyne-io/fyne-cross/internal/icon" 8 | "github.com/fyne-io/fyne-cross/internal/log" 9 | "github.com/fyne-io/fyne-cross/internal/volume" 10 | ) 11 | 12 | const ( 13 | // fyneBin is the path of the fyne binary into the docker image 14 | fyneBin = "/usr/local/bin/fyne" 15 | ) 16 | 17 | type containerEngine interface { 18 | createContainerImage(arch Architecture, OS string, image string) containerImage 19 | } 20 | 21 | type baseEngine struct { 22 | containerEngine 23 | 24 | env map[string]string // Env is the list of custom env variable to set. Specified as "KEY=VALUE" 25 | tags []string // Tags defines the tags to use 26 | 27 | vol volume.Volume 28 | } 29 | 30 | type containerImage interface { 31 | Run(vol volume.Volume, opts options, cmdArgs []string) error 32 | Prepare() error 33 | Finalize(packageName string) error 34 | 35 | Architecture() Architecture 36 | OS() string 37 | ID() string 38 | Target() string 39 | Env(string) (string, bool) 40 | SetEnv(string, string) 41 | UnsetEnv(string) 42 | AllEnv() []string 43 | SetMount(string, string, string) 44 | AppendTag(string) 45 | Tags() []string 46 | 47 | Engine() containerEngine 48 | } 49 | 50 | type containerMountPoint struct { 51 | name string 52 | localHost string 53 | inContainer string 54 | } 55 | 56 | type baseContainerImage struct { 57 | arch Architecture // Arch defines the target architecture 58 | os string // OS defines the target OS 59 | id string // ID is the context ID 60 | 61 | env map[string]string // Env is the list of custom env variable to set. Specified as "KEY=VALUE" 62 | tags []string // Tags defines the tags to use 63 | mount []containerMountPoint // Mount point between local host [key] and in container point [target] 64 | 65 | DockerImage string // DockerImage defines the docker image used to build 66 | } 67 | 68 | func newContainerEngine(context Context) (containerEngine, error) { 69 | if context.Engine.IsDocker() || context.Engine.IsPodman() { 70 | return newLocalContainerEngine(context) 71 | } 72 | if context.Engine.IsKubernetes() { 73 | return newKubernetesContainerRunner(context) 74 | } 75 | return nil, fmt.Errorf("unknown engine: '%s'", context.Engine) 76 | } 77 | 78 | var debugEnable bool 79 | 80 | func debugging() bool { 81 | return debugEnable 82 | } 83 | 84 | func (a *baseEngine) createContainerImageInternal(arch Architecture, OS string, image string, fn func(base baseContainerImage) containerImage) containerImage { 85 | var ID string 86 | 87 | if arch == "" || arch == ArchMultiple { 88 | ID = OS 89 | } else { 90 | ID = fmt.Sprintf("%s-%s", OS, arch) 91 | } 92 | 93 | ret := fn(baseContainerImage{arch: arch, os: OS, id: ID, DockerImage: image, env: make(map[string]string), tags: a.tags}) 94 | 95 | // mount the working dir 96 | ret.SetMount("project", a.vol.WorkDirHost(), a.vol.WorkDirContainer()) 97 | 98 | return ret 99 | } 100 | 101 | func (a *baseContainerImage) Architecture() Architecture { 102 | return a.arch 103 | } 104 | 105 | func (a *baseContainerImage) OS() string { 106 | return a.os 107 | } 108 | 109 | func (a *baseContainerImage) ID() string { 110 | return a.id 111 | } 112 | 113 | func (a *baseContainerImage) Target() string { 114 | target := a.OS() 115 | if target == androidOS && a.Architecture() != ArchMultiple { 116 | target += "/" + a.Architecture().String() 117 | } 118 | 119 | return target 120 | } 121 | 122 | func (a *baseContainerImage) Env(key string) (v string, ok bool) { 123 | v, ok = a.env[key] 124 | return 125 | } 126 | 127 | func (a *baseContainerImage) SetEnv(key string, value string) { 128 | a.env[key] = value 129 | } 130 | 131 | func (a *baseContainerImage) UnsetEnv(key string) { 132 | delete(a.env, key) 133 | } 134 | 135 | func (a *baseContainerImage) AllEnv() []string { 136 | r := []string{} 137 | 138 | for key, value := range a.env { 139 | r = append(r, key+"="+value) 140 | } 141 | return r 142 | } 143 | 144 | func (a *baseContainerImage) SetMount(name string, local string, inContainer string) { 145 | a.mount = append(a.mount, containerMountPoint{name: name, localHost: local, inContainer: inContainer}) 146 | } 147 | 148 | func (a *baseContainerImage) AppendTag(tag string) { 149 | a.tags = append(a.tags, tag) 150 | } 151 | 152 | func (a *baseContainerImage) Tags() []string { 153 | return a.tags 154 | } 155 | 156 | // goModInit ensure a go.mod exists. If not try to generates a temporary one 157 | func goModInit(ctx Context, image containerImage) error { 158 | if ctx.NoProjectUpload { 159 | return nil 160 | } 161 | 162 | goModPath := volume.JoinPathHost(ctx.WorkDirHost(), "go.mod") 163 | log.Infof("[i] Checking for go.mod: %s", goModPath) 164 | _, err := os.Stat(goModPath) 165 | if err == nil { 166 | log.Info("[✓] go.mod found") 167 | return nil 168 | } 169 | 170 | log.Info("[i] go.mod not found, creating a temporary one...") 171 | err = image.Run(ctx.Volume, options{}, []string{"go", "mod", "init", ctx.Name}) 172 | if err != nil { 173 | return fmt.Errorf("could not generate the temporary go module: %v", err) 174 | } 175 | 176 | log.Info("[✓] go.mod created") 177 | return nil 178 | } 179 | 180 | func fyneCommandContainer(command string, ctx Context, image containerImage) ([]string, error) { 181 | if debugging() { 182 | err := image.Run(ctx.Volume, options{}, []string{fyneBin, "version"}) 183 | if err != nil { 184 | return nil, fmt.Errorf("could not get fyne cli %s version: %v", fyneBin, err) 185 | } 186 | } 187 | 188 | icon := volume.JoinPathContainer(ctx.TmpDirContainer(), image.ID(), icon.Default) 189 | args := fyneCommand(fyneBin, command, icon, ctx, image) 190 | 191 | if ctx.Package != "." && image.OS() != androidOS { 192 | args = append(args, "-src", ctx.Package) 193 | } 194 | 195 | return args, nil 196 | } 197 | 198 | // fynePackage packages the application using the fyne cli tool 199 | func fynePackage(ctx Context, image containerImage) error { 200 | args, err := fyneCommandContainer("package", ctx, image) 201 | if err != nil { 202 | return err 203 | } 204 | 205 | // workDir default value 206 | workDir := ctx.WorkDirContainer() 207 | 208 | if image.OS() == androidOS { 209 | workDir = volume.JoinPathContainer(workDir, ctx.Package) 210 | } 211 | 212 | if ctx.StripDebug { 213 | args = append(args, "-release") 214 | } 215 | 216 | runOpts := options{ 217 | WorkDir: workDir, 218 | } 219 | 220 | err = image.Run(ctx.Volume, runOpts, args) 221 | if err != nil { 222 | return fmt.Errorf("could not package the Fyne app: %v", err) 223 | } 224 | return nil 225 | } 226 | 227 | // fyneRelease package and release the application using the fyne cli tool 228 | // Note: at the moment this is used only for the android builds 229 | func fyneRelease(ctx Context, image containerImage) error { 230 | args, err := fyneCommandContainer("release", ctx, image) 231 | if err != nil { 232 | return err 233 | } 234 | 235 | // workDir default value 236 | workDir := ctx.WorkDirContainer() 237 | 238 | switch image.OS() { 239 | case androidOS: 240 | workDir = volume.JoinPathContainer(workDir, ctx.Package) 241 | if ctx.Keystore != "" { 242 | args = append(args, "-keyStore", ctx.Keystore) 243 | } 244 | if ctx.KeystorePass != "" { 245 | args = append(args, "-keyStorePass", ctx.KeystorePass) 246 | } 247 | if ctx.KeyPass != "" { 248 | args = append(args, "-keyPass", ctx.KeyPass) 249 | } 250 | if ctx.KeyName != "" { 251 | args = append(args, "-keyName", ctx.KeyName) 252 | } 253 | case iosOS: 254 | if ctx.Certificate != "" { 255 | args = append(args, "-certificate", ctx.Certificate) 256 | } 257 | if ctx.Profile != "" { 258 | args = append(args, "-profile", ctx.Profile) 259 | } 260 | case windowsOS: 261 | if ctx.Certificate != "" { 262 | args = append(args, "-certificate", ctx.Certificate) 263 | } 264 | if ctx.Developer != "" { 265 | args = append(args, "-developer", ctx.Developer) 266 | } 267 | if ctx.Password != "" { 268 | args = append(args, "-password", ctx.Password) 269 | } 270 | } 271 | 272 | runOpts := options{ 273 | WorkDir: workDir, 274 | } 275 | 276 | err = image.Run(ctx.Volume, runOpts, args) 277 | if err != nil { 278 | return fmt.Errorf("could not package the Fyne app: %v", err) 279 | } 280 | return nil 281 | } 282 | -------------------------------------------------------------------------------- /internal/command/darwin.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "os" 7 | "runtime" 8 | 9 | "github.com/fyne-io/fyne-cross/internal/log" 10 | "github.com/fyne-io/fyne-cross/internal/volume" 11 | ) 12 | 13 | const ( 14 | // darwinOS it the darwin OS name 15 | darwinOS = "darwin" 16 | ) 17 | 18 | var ( 19 | // darwinArchSupported defines the supported target architectures on darwin 20 | darwinArchSupported = []Architecture{ArchAmd64, ArchArm64} 21 | // darwinImage is the fyne-cross image for the Darwin OS 22 | darwinImage = "fyneio/fyne-cross-images:darwin" 23 | ) 24 | 25 | // Darwin build and package the fyne app for the darwin OS 26 | type darwin struct { 27 | Images []containerImage 28 | defaultContext Context 29 | 30 | localBuild bool 31 | } 32 | 33 | var _ platformBuilder = (*darwin)(nil) 34 | var _ Command = (*darwin)(nil) 35 | 36 | func NewDarwinCommand() *darwin { 37 | return &darwin{localBuild: false} 38 | } 39 | 40 | func (cmd *darwin) Name() string { 41 | return "darwin" 42 | } 43 | 44 | // Description returns the command description 45 | func (cmd *darwin) Description() string { 46 | return "Build and package a fyne application for the darwin OS" 47 | } 48 | 49 | func (cmd *darwin) Run() error { 50 | return commonRun(cmd.defaultContext, cmd.Images, cmd) 51 | } 52 | 53 | // Parse parses the arguments and set the usage for the command 54 | func (cmd *darwin) Parse(args []string) error { 55 | commonFlags, err := newCommonFlags() 56 | if err != nil { 57 | return err 58 | } 59 | 60 | flags := &darwinFlags{ 61 | CommonFlags: commonFlags, 62 | TargetArch: &targetArchFlag{runtime.GOARCH}, 63 | } 64 | flagSet.Var(flags.TargetArch, "arch", fmt.Sprintf(`List of target architecture to build separated by comma. Supported arch: %s`, darwinArchSupported)) 65 | 66 | // Add flags to use only on darwin host 67 | if runtime.GOOS == darwinOS { 68 | flagSet.BoolVar(&cmd.localBuild, "local", true, "If set uses the fyne CLI tool installed on the host in place of the docker images") 69 | } else { 70 | flagSet.StringVar(&flags.MacOSXSDKPath, "macosx-sdk-path", "unset", "Path to macOS SDK (setting it to 'bundled' indicates that the sdk is expected to be in the container) [required]") 71 | } 72 | 73 | // flags used only in release mode 74 | flagSet.StringVar(&flags.Category, "category", "", "The category of the app for store listing") 75 | 76 | flagSet.StringVar(&flags.MacOSXVersionMin, "macosx-version-min", "unset", "Specify the minimum version that the SDK you used to create the Darwin image support") 77 | 78 | flagAppID := flagSet.Lookup("app-id") 79 | flagAppID.Usage = fmt.Sprintf("%s [required]", flagAppID.Usage) 80 | 81 | flagSet.Usage = cmd.Usage 82 | flagSet.Parse(args) 83 | 84 | err = cmd.setupContainerImages(flags, flagSet.Args()) 85 | return err 86 | } 87 | 88 | // Run runs the command 89 | func (cmd *darwin) Build(image containerImage) (string, error) { 90 | err := prepareIcon(cmd.defaultContext, image) 91 | if err != nil { 92 | return "", err 93 | } 94 | 95 | // 96 | // package 97 | // 98 | log.Info("[i] Packaging app...") 99 | 100 | var packageName string 101 | if cmd.defaultContext.Release { 102 | if runtime.GOOS != darwinOS { 103 | return "", fmt.Errorf("darwin release build is supported only on darwin hosts") 104 | } 105 | 106 | packageName, err = fyneReleaseHost(cmd.defaultContext, image) 107 | if err != nil { 108 | return "", fmt.Errorf("could not package the Fyne app: %v", err) 109 | } 110 | 111 | } else if cmd.localBuild { 112 | packageName, err = fynePackageHost(cmd.defaultContext, image) 113 | if err != nil { 114 | return "", fmt.Errorf("could not package the Fyne app: %v", err) 115 | } 116 | } else { 117 | packageName = fmt.Sprintf("%s.app", cmd.defaultContext.Name) 118 | 119 | err = fynePackage(cmd.defaultContext, image) 120 | if err != nil { 121 | return "", fmt.Errorf("could not package the Fyne app: %v", err) 122 | } 123 | } 124 | 125 | // move the dist package into the expected tmp/$ID/packageName location in the container 126 | image.Run(cmd.defaultContext.Volume, options{}, []string{ 127 | "mv", 128 | volume.JoinPathContainer(cmd.defaultContext.WorkDirContainer(), packageName), 129 | volume.JoinPathContainer(cmd.defaultContext.TmpDirContainer(), image.ID(), packageName), 130 | }) 131 | 132 | // copy the binary into the expected bin/$ID/packageName location in the container 133 | image.Run(cmd.defaultContext.Volume, options{}, 134 | []string{ 135 | "sh", "-c", fmt.Sprintf("cp %q/* %q", 136 | volume.JoinPathContainer(cmd.defaultContext.TmpDirContainer(), image.ID(), packageName, "Contents", "MacOS"), 137 | volume.JoinPathContainer(cmd.defaultContext.BinDirContainer(), image.ID()), 138 | ), 139 | }) 140 | 141 | return packageName, nil 142 | } 143 | 144 | // Usage displays the command usage 145 | func (cmd *darwin) Usage() { 146 | data := struct { 147 | Name string 148 | Description string 149 | }{ 150 | Name: cmd.Name(), 151 | Description: cmd.Description(), 152 | } 153 | 154 | template := ` 155 | Usage: fyne-cross {{ .Name }} [options] [package] 156 | 157 | {{ .Description }} 158 | 159 | Options: 160 | ` 161 | 162 | printUsage(template, data) 163 | flagSet.PrintDefaults() 164 | } 165 | 166 | // darwinFlags defines the command-line flags for the darwin command 167 | type darwinFlags struct { 168 | *CommonFlags 169 | 170 | //Category represents the category of the app for store listing 171 | Category string 172 | 173 | // TargetArch represents a list of target architecture to build on separated by comma 174 | TargetArch *targetArchFlag 175 | 176 | // Specify MacOSX minimum version 177 | MacOSXVersionMin string 178 | 179 | // MacOSXSDKPath represents the MacOSX SDK path on host 180 | MacOSXSDKPath string 181 | } 182 | 183 | // setupContainerImages returns the command context for a darwin target 184 | func (cmd *darwin) setupContainerImages(flags *darwinFlags, args []string) error { 185 | targetArch, err := targetArchFromFlag(*flags.TargetArch, darwinArchSupported) 186 | if err != nil { 187 | return fmt.Errorf("could not make command context for %s OS: %s", darwinOS, err) 188 | } 189 | 190 | ctx, err := makeDefaultContext(flags.CommonFlags, args) 191 | if err != nil { 192 | return err 193 | } 194 | 195 | if ctx.AppID == "" { 196 | return errors.New("appID is mandatory") 197 | } 198 | 199 | ctx.Category = flags.Category 200 | 201 | // Following settings are needed to cross compile with zig 202 | ctx.BuildMode = "pie" 203 | 204 | cmd.defaultContext = ctx 205 | runner, err := newContainerEngine(ctx) 206 | if err != nil { 207 | return err 208 | } 209 | 210 | for _, arch := range targetArch { 211 | var image containerImage 212 | var zigTarget string 213 | switch arch { 214 | case ArchAmd64: 215 | minVer := "10.12" 216 | if flags.MacOSXVersionMin != "unset" { 217 | minVer = flags.MacOSXVersionMin 218 | } 219 | zigTarget = "x86_64-macos." + minVer 220 | image = runner.createContainerImage(arch, darwinOS, overrideDockerImage(flags.CommonFlags, darwinImage)) 221 | image.SetEnv("GOARCH", "amd64") 222 | case ArchArm64: 223 | minVer := "11.1" 224 | if flags.MacOSXVersionMin != "unset" { 225 | minVer = flags.MacOSXVersionMin 226 | } 227 | zigTarget = "aarch64-macos." + minVer 228 | image = runner.createContainerImage(arch, darwinOS, overrideDockerImage(flags.CommonFlags, darwinImage)) 229 | image.SetEnv("GOARCH", "arm64") 230 | } 231 | zigCC := fmt.Sprintf("zig cc -v -target %s -isysroot /sdk -iwithsysroot /usr/include -iframeworkwithsysroot /System/Library/Frameworks", zigTarget) 232 | zigCXX := fmt.Sprintf("zig c++ -v -target %s -isysroot /sdk -iwithsysroot /usr/include -iframeworkwithsysroot /System/Library/Frameworks", zigTarget) 233 | image.SetEnv("CC", zigCC) 234 | image.SetEnv("CXX", zigCXX) 235 | image.SetEnv("CGO_LDFLAGS", "--sysroot /sdk -F/System/Library/Frameworks -L/usr/lib") 236 | image.SetEnv("GOOS", "darwin") 237 | 238 | if !cmd.localBuild { 239 | if flags.MacOSXSDKPath == "unset" { 240 | // This is checking if the provided container image has the macOSX SDK installed 241 | err := image.Run(ctx.Volume, options{}, []string{"sh", "-c", "ls /sdk/usr/include/stdlib.h 2>/dev/null >/dev/null"}) 242 | if err != nil { 243 | return errors.New("macOSX SDK path is mandatory") 244 | } 245 | } else if flags.MacOSXSDKPath != "bundled" { 246 | if _, err := os.Stat(flags.MacOSXSDKPath); os.IsNotExist(err) { 247 | return errors.New("macOSX SDK path does not exists") 248 | } 249 | image.SetMount("sdk", flags.MacOSXSDKPath, "/sdk") 250 | } 251 | } 252 | 253 | cmd.Images = append(cmd.Images, image) 254 | } 255 | 256 | return nil 257 | } 258 | -------------------------------------------------------------------------------- /internal/command/context_test.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | "github.com/stretchr/testify/require" 9 | 10 | "github.com/fyne-io/fyne-cross/internal/volume" 11 | ) 12 | 13 | func Test_makeDefaultContext(t *testing.T) { 14 | vol, err := mockDefaultVolume() 15 | require.Nil(t, err) 16 | 17 | engine, err := MakeEngine(autodetectEngine) 18 | if err != nil { 19 | t.Skip("engine not found", err) 20 | } 21 | 22 | type args struct { 23 | flags *CommonFlags 24 | args []string 25 | } 26 | tests := []struct { 27 | name string 28 | args args 29 | want Context 30 | wantErr bool 31 | }{ 32 | { 33 | name: "default", 34 | args: args{ 35 | flags: &CommonFlags{ 36 | AppBuild: 1, 37 | }, 38 | }, 39 | want: Context{ 40 | AppBuild: "1", 41 | AppID: "", 42 | Volume: vol, 43 | CacheEnabled: true, 44 | StripDebug: true, 45 | Package: ".", 46 | Engine: engine, 47 | Env: map[string]string{}, 48 | }, 49 | wantErr: false, 50 | }, 51 | { 52 | name: "custom env", 53 | args: args{ 54 | flags: &CommonFlags{ 55 | AppBuild: 1, 56 | Env: envFlag{"TEST=true"}, 57 | }, 58 | }, 59 | want: Context{ 60 | AppBuild: "1", 61 | AppID: "", 62 | Volume: vol, 63 | CacheEnabled: true, 64 | StripDebug: true, 65 | Package: ".", 66 | Engine: engine, 67 | Env: map[string]string{"TEST": "true"}, 68 | }, 69 | wantErr: false, 70 | }, 71 | { 72 | name: "custom env containg =", 73 | args: args{ 74 | flags: &CommonFlags{ 75 | AppBuild: 1, 76 | Env: envFlag{"GOFLAGS=-mod=vendor"}, 77 | }, 78 | }, 79 | want: Context{ 80 | AppBuild: "1", 81 | AppID: "", 82 | Volume: vol, 83 | CacheEnabled: true, 84 | StripDebug: true, 85 | Package: ".", 86 | Engine: engine, 87 | Env: map[string]string{"GOFLAGS": "-mod=vendor"}, 88 | }, 89 | wantErr: false, 90 | }, 91 | { 92 | name: "custom ldflags", 93 | args: args{ 94 | flags: &CommonFlags{ 95 | AppBuild: 1, 96 | Ldflags: "-X main.version=1.2.3", 97 | }, 98 | }, 99 | want: Context{ 100 | AppBuild: "1", 101 | AppID: "", 102 | Volume: vol, 103 | CacheEnabled: true, 104 | StripDebug: true, 105 | Package: ".", 106 | Engine: engine, 107 | Env: map[string]string{ 108 | "GOFLAGS": "-ldflags=-X -ldflags=main.version=1.2.3", 109 | }, 110 | }, 111 | wantErr: false, 112 | }, 113 | { 114 | name: "package default", 115 | args: args{ 116 | flags: &CommonFlags{ 117 | AppBuild: 1, 118 | }, 119 | }, 120 | want: Context{ 121 | AppBuild: "1", 122 | AppID: "", 123 | Volume: vol, 124 | CacheEnabled: true, 125 | StripDebug: true, 126 | Package: ".", 127 | Engine: engine, 128 | Env: map[string]string{}, 129 | }, 130 | wantErr: false, 131 | }, 132 | { 133 | name: "package dot", 134 | args: args{ 135 | flags: &CommonFlags{ 136 | AppBuild: 1, 137 | }, 138 | args: []string{"."}, 139 | }, 140 | want: Context{ 141 | AppBuild: "1", 142 | AppID: "", 143 | Volume: vol, 144 | CacheEnabled: true, 145 | StripDebug: true, 146 | Package: ".", 147 | Engine: engine, 148 | Env: map[string]string{}, 149 | }, 150 | wantErr: false, 151 | }, 152 | { 153 | name: "package relative", 154 | args: args{ 155 | flags: &CommonFlags{ 156 | AppBuild: 1, 157 | }, 158 | args: []string{"./cmd/command"}, 159 | }, 160 | want: Context{ 161 | AppBuild: "1", 162 | AppID: "", 163 | Volume: vol, 164 | CacheEnabled: true, 165 | StripDebug: true, 166 | Package: "./cmd/command", 167 | Engine: engine, 168 | Env: map[string]string{}, 169 | }, 170 | wantErr: false, 171 | }, 172 | { 173 | name: "package absolute", 174 | args: args{ 175 | flags: &CommonFlags{ 176 | AppBuild: 1, 177 | }, 178 | args: []string{volume.JoinPathHost(vol.WorkDirHost(), "cmd/command")}, 179 | }, 180 | want: Context{ 181 | AppBuild: "1", 182 | AppID: "", 183 | Volume: vol, 184 | CacheEnabled: true, 185 | StripDebug: true, 186 | Package: "./cmd/command", 187 | Engine: engine, 188 | Env: map[string]string{}, 189 | }, 190 | wantErr: false, 191 | }, 192 | { 193 | name: "package absolute outside work dir", 194 | args: args{ 195 | flags: &CommonFlags{ 196 | AppBuild: 1, 197 | }, 198 | args: []string{os.TempDir()}, 199 | }, 200 | wantErr: true, 201 | }, 202 | { 203 | name: "custom tags", 204 | args: args{ 205 | flags: &CommonFlags{ 206 | AppBuild: 1, 207 | Tags: tagsFlag{"hints", "gles"}, 208 | }, 209 | }, 210 | want: Context{ 211 | AppBuild: "1", 212 | AppID: "", 213 | Volume: vol, 214 | CacheEnabled: true, 215 | StripDebug: true, 216 | Package: ".", 217 | Tags: []string{"hints", "gles"}, 218 | Engine: engine, 219 | Env: map[string]string{}, 220 | }, 221 | wantErr: false, 222 | }, 223 | { 224 | name: "invalid app build", 225 | args: args{ 226 | flags: &CommonFlags{ 227 | AppBuild: 0, 228 | }, 229 | }, 230 | want: Context{}, 231 | wantErr: true, 232 | }, 233 | { 234 | name: "release mode enabled", 235 | args: args{ 236 | flags: &CommonFlags{ 237 | AppBuild: 1, 238 | Release: true, 239 | }, 240 | }, 241 | want: Context{ 242 | AppBuild: "1", 243 | AppID: "", 244 | Volume: vol, 245 | CacheEnabled: true, 246 | StripDebug: true, 247 | Package: ".", 248 | Release: true, 249 | Engine: engine, 250 | Env: map[string]string{}, 251 | }, 252 | wantErr: false, 253 | }, 254 | { 255 | name: "app version", 256 | args: args{ 257 | flags: &CommonFlags{ 258 | AppBuild: 1, 259 | AppVersion: "1.0", 260 | Release: true, 261 | }, 262 | }, 263 | want: Context{ 264 | AppBuild: "1", 265 | AppID: "", 266 | AppVersion: "1.0", 267 | Volume: vol, 268 | CacheEnabled: true, 269 | StripDebug: true, 270 | Package: ".", 271 | Release: true, 272 | Engine: engine, 273 | Env: map[string]string{}, 274 | }, 275 | wantErr: false, 276 | }, 277 | { 278 | name: "deprecate output flag in favour of name", 279 | args: args{ 280 | flags: &CommonFlags{ 281 | AppBuild: 1, 282 | Name: "./test", 283 | }, 284 | }, 285 | want: Context{ 286 | AppBuild: "1", 287 | AppID: "", 288 | Volume: vol, 289 | CacheEnabled: true, 290 | StripDebug: true, 291 | Package: ".", 292 | Name: "./test", 293 | Engine: engine, 294 | Env: map[string]string{}, 295 | }, 296 | wantErr: true, 297 | }, 298 | { 299 | name: "valid name", 300 | args: args{ 301 | flags: &CommonFlags{ 302 | AppBuild: 1, 303 | Name: "test", 304 | }, 305 | }, 306 | want: Context{ 307 | AppBuild: "1", 308 | AppID: "", 309 | Volume: vol, 310 | CacheEnabled: true, 311 | StripDebug: true, 312 | Package: ".", 313 | Name: "test", 314 | Engine: engine, 315 | Env: map[string]string{}, 316 | }, 317 | wantErr: false, 318 | }, 319 | { 320 | name: "appID", 321 | args: args{ 322 | flags: &CommonFlags{ 323 | AppBuild: 1, 324 | AppID: "com.example.test", 325 | Name: "test", 326 | }, 327 | }, 328 | want: Context{ 329 | AppBuild: "1", 330 | AppID: "com.example.test", 331 | Volume: vol, 332 | CacheEnabled: true, 333 | StripDebug: true, 334 | Package: ".", 335 | Name: "test", 336 | Engine: engine, 337 | Env: map[string]string{}, 338 | }, 339 | wantErr: false, 340 | }, 341 | } 342 | for _, tt := range tests { 343 | t.Run(tt.name, func(t *testing.T) { 344 | ctx, err := makeDefaultContext(tt.args.flags, tt.args.args) 345 | 346 | if tt.wantErr { 347 | require.NotNil(t, err) 348 | return 349 | } 350 | require.Nil(t, err) 351 | assert.Equal(t, tt.want, ctx) 352 | }) 353 | } 354 | } 355 | 356 | func mockDefaultVolume() (volume.Volume, error) { 357 | rootDir, err := volume.DefaultWorkDirHost() 358 | if err != nil { 359 | return volume.Volume{}, err 360 | } 361 | cacheDir, err := volume.DefaultCacheDirHost() 362 | if err != nil { 363 | return volume.Volume{}, err 364 | } 365 | return volume.Mount(rootDir, cacheDir) 366 | } 367 | -------------------------------------------------------------------------------- /internal/command/context.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "fmt" 7 | "os" 8 | "path/filepath" 9 | "runtime" 10 | "strconv" 11 | "strings" 12 | 13 | "github.com/fyne-io/fyne-cross/internal/log" 14 | "github.com/fyne-io/fyne-cross/internal/volume" 15 | ) 16 | 17 | const ( 18 | // ArchAmd64 represents the amd64 architecture 19 | ArchAmd64 Architecture = "amd64" 20 | // Arch386 represents the amd64 architecture 21 | Arch386 Architecture = "386" 22 | // ArchArm64 represents the arm64 architecture 23 | ArchArm64 Architecture = "arm64" 24 | // ArchArm represents the arm architecture 25 | ArchArm Architecture = "arm" 26 | // ArchMultiple represents the universal architecture used by some OS to 27 | // identify a binary that supports multiple architectures (fat binary) 28 | ArchMultiple Architecture = "multiple" 29 | ) 30 | 31 | // Architecture represents the Architecture type 32 | type Architecture string 33 | 34 | func (a Architecture) String() string { 35 | return (string)(a) 36 | } 37 | 38 | // Context represent a build context 39 | type Context struct { 40 | // Volume holds the mounted volumes between host and containers 41 | volume.Volume 42 | 43 | Engine Engine // Engine is the container engine to use 44 | Namespace string // Namespace used by Kubernetes engine to run its pod in 45 | S3Path string // Project base directory to use to push and pull data from S3 46 | SizeLimit string // Container mount point size limits honored by Kubernetes only 47 | Env map[string]string // Env is the list of custom env variable to set. Specified as "KEY=VALUE" 48 | Tags []string // Tags defines the tags to use 49 | Metadata map[string]string // Metadata contain custom metadata passed to fyne package 50 | 51 | AppBuild string // Build number 52 | AppID string // AppID is the appID to use for distribution 53 | AppVersion string // AppVersion is the version number in the form x, x.y or x.y.z semantic version 54 | CacheEnabled bool // CacheEnabled if true enable go build cache 55 | Icon string // Icon is the optional icon in png format to use for distribution 56 | Name string // Name is the application name 57 | Package string // Package is the package to build named by the import path as per 'go build' 58 | Release bool // Enable release mode. If true, prepares an application for public distribution 59 | StripDebug bool // StripDebug if true, strips binary output 60 | Debug bool // Debug if true enable debug log 61 | Pull bool // Pull if true attempts to pull a newer version of the docker image 62 | NoProjectUpload bool // NoProjectUpload if true, the build will be done with the artifact already stored on S3 63 | NoResultDownload bool // NoResultDownload if true, the result of the build will be left on S3 and not downloaded locally 64 | NoNetwork bool // NoNetwork if true, the build will be done without network access 65 | 66 | //Build context 67 | BuildMode string // The -buildmode argument to pass to go build 68 | 69 | // Release context 70 | Category string //Category represents the category of the app for store listing [macOS] 71 | Certificate string //Certificate represents the name of the certificate to sign the build [iOS, Windows] 72 | Developer string //Developer represents the developer identity for your Microsoft store account [Windows] 73 | Keystore string //Keystore represents the location of .keystore file containing signing information [Android] 74 | KeystorePass string //KeystorePass represents the password for the .keystore file [Android] 75 | KeyPass string //KeyPass represents the assword for the signer's private key, which is needed if the private key is password-protected [Android] 76 | KeyName string //KeyName represents the name of the key to sign the build [Android] 77 | Password string //Password represents the password for the certificate used to sign the build [Windows] 78 | Profile string //Profile represents the name of the provisioning profile for this release build [iOS] 79 | } 80 | 81 | // String implements the Stringer interface 82 | func (ctx Context) String() string { 83 | buf := &bytes.Buffer{} 84 | 85 | template := ` 86 | Architecture: {{ .Architecture }} 87 | OS: {{ .OS }} 88 | Name: {{ .Name }} 89 | ` 90 | 91 | log.PrintTemplate(buf, template, ctx) 92 | return buf.String() 93 | } 94 | 95 | func overrideDockerImage(flags *CommonFlags, image string) string { 96 | if flags.DockerImage != "" { 97 | return flags.DockerImage 98 | } 99 | 100 | if flags.DockerRegistry != "" { 101 | return fmt.Sprintf("%s/%s", flags.DockerRegistry, image) 102 | } 103 | 104 | return image 105 | } 106 | 107 | func makeDefaultContext(flags *CommonFlags, args []string) (Context, error) { 108 | // mount the fyne-cross volume 109 | vol, err := volume.Mount(flags.RootDir, flags.CacheDir) 110 | if err != nil { 111 | return Context{}, err 112 | } 113 | 114 | engine := flags.Engine.Engine 115 | if (engine == Engine{}) { 116 | if flags.Namespace != "" && flags.Namespace != "default" { 117 | engine, err = MakeEngine(kubernetesEngine) 118 | if err != nil { 119 | return Context{}, err 120 | } 121 | } else { 122 | // attempt to autodetect 123 | engine, err = MakeEngine(autodetectEngine) 124 | if err != nil { 125 | return Context{}, err 126 | } 127 | } 128 | } 129 | 130 | // set context based on command-line flags 131 | ctx := Context{ 132 | AppID: flags.AppID, 133 | AppVersion: flags.AppVersion, 134 | CacheEnabled: !flags.NoCache, 135 | NoProjectUpload: flags.NoProjectUpload, 136 | NoResultDownload: flags.NoResultDownload, 137 | Engine: engine, 138 | Namespace: flags.Namespace, 139 | S3Path: flags.S3Path, 140 | SizeLimit: flags.SizeLimit, 141 | Env: make(map[string]string), 142 | Tags: flags.Tags, 143 | Metadata: flags.Metadata.values, 144 | Icon: flags.Icon, 145 | Name: flags.Name, 146 | StripDebug: !flags.NoStripDebug, 147 | Debug: flags.Debug, 148 | NoNetwork: flags.NoNetwork, 149 | Volume: vol, 150 | Pull: flags.Pull, 151 | Release: flags.Release, 152 | } 153 | 154 | if flags.AppBuild <= 0 { 155 | return ctx, errors.New("build number should be greater than 0") 156 | } 157 | 158 | // the flag name that replace the deprecated output should not be used 159 | // as path. Returns error if contains \ or / 160 | // Fixes: #9 161 | // TODO: update the error message once the output flag is removed 162 | if strings.ContainsAny(flags.Name, "\\/") { 163 | return ctx, errors.New("output and app name should not be used as path") 164 | } 165 | 166 | for _, v := range flags.Env { 167 | parts := strings.SplitN(v, "=", 2) 168 | ctx.Env[parts[0]] = parts[1] 169 | } 170 | 171 | ctx.AppBuild = strconv.Itoa(flags.AppBuild) 172 | 173 | ctx.Package, err = packageFromArgs(args, vol) 174 | if err != nil { 175 | return ctx, err 176 | } 177 | 178 | if env := os.Getenv("GOFLAGS"); env != "" { 179 | ctx.Env["GOFLAGS"] = env 180 | } 181 | 182 | if len(flags.Ldflags) > 0 { 183 | goflags := "" 184 | for _, ldflags := range strings.Fields(flags.Ldflags) { 185 | goflags += "-ldflags=" + ldflags + " " 186 | } 187 | if v, ok := ctx.Env["GOFLAGS"]; ok { 188 | ctx.Env["GOFLAGS"] = strings.TrimSpace(v + " " + goflags) 189 | } else { 190 | ctx.Env["GOFLAGS"] = strings.TrimSpace(goflags) 191 | } 192 | } 193 | 194 | if flags.Silent { 195 | log.SetLevel(log.LevelSilent) 196 | } 197 | 198 | if flags.Debug { 199 | log.SetLevel(log.LevelDebug) 200 | debugEnable = true 201 | } 202 | 203 | return ctx, nil 204 | } 205 | 206 | // packageFromArgs validates and returns the package to compile. 207 | func packageFromArgs(args []string, vol volume.Volume) (string, error) { 208 | pkg := "." 209 | if len(args) > 0 { 210 | pkg = args[0] 211 | } 212 | if pkg == "." { 213 | return ".", nil 214 | } 215 | 216 | if !filepath.IsAbs(pkg) { 217 | return pkg, nil 218 | } 219 | 220 | pkg = filepath.Clean(pkg) 221 | 222 | if !strings.HasPrefix(pkg, vol.WorkDirHost()) { 223 | return pkg, fmt.Errorf("package options when specified as absolute path must be relative to the project root dir") 224 | } 225 | 226 | pkg = strings.Replace(pkg, vol.WorkDirHost(), ".", 1) 227 | if runtime.GOOS == "windows" { 228 | pkg = filepath.ToSlash(pkg) 229 | } 230 | return pkg, nil 231 | } 232 | 233 | // targetArchFromFlag validates and returns the architecture specified using flag against the supported ones. 234 | // If flagVar contains the wildcard char "*" all the supported architecture are returned. 235 | func targetArchFromFlag(flagVar []string, supportedArch []Architecture) ([]Architecture, error) { 236 | targetArch := []Architecture{} 237 | Loop: 238 | for _, v := range flagVar { 239 | if v == "*" { 240 | return supportedArch, nil 241 | } 242 | for _, valid := range supportedArch { 243 | if Architecture(v) == valid { 244 | targetArch = append(targetArch, valid) 245 | continue Loop 246 | } 247 | } 248 | return nil, fmt.Errorf("arch %q is not supported. Supported: %s", v, supportedArch) 249 | } 250 | return targetArch, nil 251 | } 252 | -------------------------------------------------------------------------------- /internal/command/flag.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | import ( 4 | "errors" 5 | "flag" 6 | "fmt" 7 | "os" 8 | "path/filepath" 9 | "strings" 10 | 11 | "github.com/fyne-io/fyne-cross/internal/icon" 12 | "github.com/fyne-io/fyne-cross/internal/metadata" 13 | "github.com/fyne-io/fyne-cross/internal/volume" 14 | ) 15 | 16 | var flagSet = flag.NewFlagSet("fyne-cross", flag.ExitOnError) 17 | 18 | // CommonFlags holds the flags shared between all commands 19 | type CommonFlags struct { 20 | // AppBuild represents the build number, should be greater than 0 and 21 | // incremented for each build 22 | AppBuild int 23 | // AppID represents the application ID used for distribution 24 | AppID string 25 | // AppVersion represents the version number in the form x, x.y or x.y.z semantic version 26 | AppVersion string 27 | // CacheDir is the directory used to share/cache sources and dependencies. 28 | // Default to system cache directory (i.e. $HOME/.cache/fyne-cross) 29 | CacheDir string 30 | // DockerImage represents a custom docker image to use for build 31 | DockerImage string 32 | // Engine is the container engine to use 33 | Engine engineFlag 34 | // Namespace used by Kubernetes engine to run its pod in 35 | Namespace string 36 | // Base S3 directory to push and pull data from 37 | S3Path string 38 | // Container mount point size limits honored by Kubernetes only 39 | SizeLimit string 40 | // Env is the list of custom env variable to set. Specified as "KEY=VALUE" 41 | Env envFlag 42 | // Icon represents the application icon used for distribution 43 | Icon string 44 | // Ldflags represents the flags to pass to the external linker 45 | Ldflags string 46 | // Additional build tags 47 | Tags tagsFlag 48 | // Metadata contain custom metadata passed to fyne package 49 | Metadata multiFlags 50 | // NoCache if true will not use the go build cache 51 | NoCache bool 52 | // NoProjectUpload if true, the build will be done with the artifact already stored on S3 53 | NoProjectUpload bool 54 | // NoResultDownload if true, it will leave the result of the build on S3 and won't download it locally (engine: kubernetes) 55 | NoResultDownload bool 56 | // NoStripDebug if true will not strip debug information from binaries 57 | NoStripDebug bool 58 | // NoNetwork if true will not setup network inside the container 59 | NoNetwork bool 60 | // Name represents the application name 61 | Name string 62 | // Release represents if the package should be prepared for release (disable debug etc) 63 | Release bool 64 | // RootDir represents the project root directory 65 | RootDir string 66 | // Silent enables the silent mode 67 | Silent bool 68 | // Debug enables the debug mode 69 | Debug bool 70 | // Pull attempts to pull a newer version of the docker image 71 | Pull bool 72 | // DockerRegistry changes the pull/push docker registry (defualt docker.io) 73 | DockerRegistry string 74 | } 75 | 76 | // newCommonFlags defines all the flags for the shared options 77 | func newCommonFlags() (*CommonFlags, error) { 78 | name, err := defaultName() 79 | if err != nil { 80 | return nil, err 81 | } 82 | rootDir, err := volume.DefaultWorkDirHost() 83 | if err != nil { 84 | return nil, err 85 | } 86 | cacheDir, err := volume.DefaultCacheDirHost() 87 | if err != nil { 88 | return nil, err 89 | } 90 | 91 | defaultIcon := icon.Default 92 | appID := "" 93 | appVersion := "1.0.0" 94 | appBuild := 1 95 | 96 | data, _ := metadata.LoadStandard(rootDir) 97 | if data != nil { 98 | if data.Details.Icon != "" { 99 | defaultIcon = data.Details.Icon 100 | } 101 | if data.Details.Name != "" { 102 | name = data.Details.Name 103 | } 104 | if data.Details.ID != "" { 105 | appID = data.Details.ID 106 | } 107 | if data.Details.Version != "" { 108 | appVersion = data.Details.Version 109 | } 110 | if data.Details.Build != 0 { 111 | appBuild = data.Details.Build 112 | } 113 | } 114 | 115 | flags := &CommonFlags{} 116 | kubernetesFlagSet(flagSet, flags) 117 | flagSet.IntVar(&flags.AppBuild, "app-build", appBuild, "Build number, should be greater than 0 and incremented for each build") 118 | flagSet.StringVar(&flags.AppID, "app-id", appID, "Application ID used for distribution") 119 | flagSet.StringVar(&flags.AppVersion, "app-version", appVersion, "Version number in the form x, x.y or x.y.z semantic version") 120 | flagSet.StringVar(&flags.CacheDir, "cache", cacheDir, "Directory used to share/cache sources and dependencies") 121 | flagSet.BoolVar(&flags.NoCache, "no-cache", false, "Do not use the go build cache") 122 | flagSet.Var(&flags.Engine, "engine", "The container engine to use. Supported engines: [docker, podman, kubernetes]. Default to autodetect.") 123 | flagSet.Var(&flags.Env, "env", "List of additional env variables specified as KEY=VALUE") 124 | flagSet.StringVar(&flags.Icon, "icon", defaultIcon, "Application icon used for distribution") 125 | flagSet.StringVar(&flags.DockerImage, "image", "", "Custom docker image to use for build") 126 | flagSet.StringVar(&flags.Ldflags, "ldflags", "", "Additional flags to pass to the external linker") 127 | flagSet.Var(&flags.Tags, "tags", "List of additional build tags separated by comma") 128 | flagSet.Var(&flags.Metadata, "metadata", "Additional metadata `key=value` passed to fyne package") 129 | flagSet.BoolVar(&flags.NoStripDebug, "no-strip-debug", false, "Do not strip debug information from binaries") 130 | flagSet.StringVar(&flags.Name, "name", name, "The name of the application") 131 | flagSet.StringVar(&flags.Name, "output", name, "Named output file. Deprecated in favour of 'name'") 132 | flagSet.BoolVar(&flags.Release, "release", false, "Release mode. Prepares the application for public distribution") 133 | flagSet.StringVar(&flags.RootDir, "dir", rootDir, "Fyne app root directory") 134 | flagSet.BoolVar(&flags.Silent, "silent", false, "Silent mode") 135 | flagSet.BoolVar(&flags.Debug, "debug", false, "Debug mode") 136 | flagSet.BoolVar(&flags.Pull, "pull", false, "Attempt to pull a newer version of the docker image") 137 | flagSet.StringVar(&flags.DockerRegistry, "docker-registry", "docker.io", "The docker registry to be used instead of dockerhub (only used with defualt docker images)") 138 | flagSet.BoolVar(&flags.NoNetwork, "no-network", false, "If set, the build will be done without network access") 139 | return flags, nil 140 | } 141 | 142 | func defaultName() (string, error) { 143 | wd, err := os.Getwd() 144 | if err != nil { 145 | return "", fmt.Errorf("cannot get the path for current directory %s", err) 146 | } 147 | _, output := filepath.Split(wd) 148 | return output, nil 149 | } 150 | 151 | // engineFlag is a custom flag used to define custom engine variables 152 | type engineFlag struct { 153 | Engine 154 | } 155 | 156 | // String is the method to format the flag's value, part of the flag.Value interface. 157 | // The String method's output will be used in diagnostics. 158 | func (ef *engineFlag) String() string { 159 | return fmt.Sprint(*ef) 160 | } 161 | 162 | // Set is the method to set the flag value, part of the flag.Value interface. 163 | // Set's argument is a string to be parsed to set the flag. 164 | func (ef *engineFlag) Set(value string) error { 165 | var err error 166 | ef.Engine, err = MakeEngine(value) 167 | return err 168 | } 169 | 170 | // envFlag is a custom flag used to define custom env variables 171 | type envFlag []string 172 | 173 | // String is the method to format the flag's value, part of the flag.Value interface. 174 | // The String method's output will be used in diagnostics. 175 | func (ef *envFlag) String() string { 176 | return fmt.Sprint(*ef) 177 | } 178 | 179 | // Set is the method to set the flag value, part of the flag.Value interface. 180 | // Set's argument is a string to be parsed to set the flag. 181 | func (ef *envFlag) Set(value string) error { 182 | if !strings.Contains(value, "=") { 183 | return errors.New("env var must defined as KEY=VALUE or KEY=") 184 | } 185 | *ef = append(*ef, value) 186 | 187 | return nil 188 | } 189 | 190 | // targetArchFlag is a custom flag used to define architectures 191 | type targetArchFlag []string 192 | 193 | // String is the method to format the flag's value, part of the flag.Value interface. 194 | // The String method's output will be used in diagnostics. 195 | func (af *targetArchFlag) String() string { 196 | return fmt.Sprint(*af) 197 | } 198 | 199 | // Set is the method to set the flag value, part of the flag.Value interface. 200 | // Set's argument is a string to be parsed to set the flag. 201 | // It's a comma-separated list, so we split it. 202 | func (af *targetArchFlag) Set(value string) error { 203 | *af = []string{} 204 | if len(*af) > 1 { 205 | return errors.New("flag already set") 206 | } 207 | 208 | for _, v := range strings.Split(value, ",") { 209 | *af = append(*af, strings.TrimSpace(v)) 210 | } 211 | return nil 212 | } 213 | 214 | // tagsFlag is a custom flag used to define build tags 215 | type tagsFlag []string 216 | 217 | // String is the method to format the flag's value, part of the flag.Value interface. 218 | // The String method's output will be used in diagnostics. 219 | func (tf *tagsFlag) String() string { 220 | return fmt.Sprint(*tf) 221 | } 222 | 223 | // Set is the method to set the flag value, part of the flag.Value interface. 224 | // Set's argument is a string to be parsed to set the flag. 225 | // It's a comma-separated list, so we split it. 226 | func (tf *tagsFlag) Set(value string) error { 227 | *tf = []string{} 228 | if len(*tf) > 1 { 229 | return errors.New("flag already set") 230 | } 231 | 232 | for _, v := range strings.Split(value, ",") { 233 | *tf = append(*tf, strings.TrimSpace(v)) 234 | } 235 | return nil 236 | } 237 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog - Fyne.io fyne-cross 2 | 3 | ## 1.6.1 - 12 Jan 2025 4 | 5 | ### Changed 6 | 7 | - fix: update install suggestion for missing `fyne` command by @nobe4 in 8 | - Performance improvement by avoiding running commands in docker by @williambrode in 9 | - Fixing so we don't seek a missing android image for darwin by @andydotxyz in 10 | - Fix Android release that generates a .aab file by @metal3d in 11 | - readme: update the requirements and the macos SDK extract section by @lucor in 12 | - metadata: update Fyne metadata to v2.5.3 by @lucor in 13 | - web: update destination folder by @lucor in 14 | - Bump k8s.io/api from 0.18.19 to 0.30.2 by @dependabot in 15 | - Bump github.com/klauspost/compress from 1.13.4 to 1.17.9 by @dependabot in 16 | - Bump github.com/urfave/cli/v2 from 2.11.1 to 2.27.2 by @dependabot in 17 | 18 | ## 1.6.0 - 31 Dec 2024 19 | 20 | ### Changed 21 | 22 | - Bump github.com/stretchr/testify from 1.7.0 to 1.9.0 by @dependabot in 23 | - Ldflags where only needed with older version of zig which we have updated since then. by @Bluebugs in 24 | 25 | ## 1.5.0 - 13 Apr 2024 26 | 27 | ### Changed 28 | 29 | - Improve Docker Darwin support when using it has a host for fyne-cross (ssh-agent detection, documentation, arm64) 30 | - Support Podman on Darwin 31 | - Improve Android signature support 32 | - Propagate GOFLAGS correctly 33 | - Adjust supported Go version to match Fyne 34 | - Update dependencies 35 | 36 | ## 1.4.0 - 13 Mar 2023 37 | 38 | ### Added 39 | 40 | - Add support for Kubernetes 41 | - Add ability to specify a different registry 42 | - Support for fyne metadata 43 | 44 | ### Changed 45 | 46 | - Pull image from fyne-cross-image repository 47 | - Simplify `fyne-cross darwin-sdk-extract` by getting the needed files from the Apple SDK and then mounting them in the container image for each build 48 | - Provide a darwin image and mount the SDK from the host 49 | - Use `fyne build` for all targets 50 | 51 | ## 1.3.0 - 16 Jul 2022 52 | 53 | ### Added 54 | 55 | - Add support for web target #92 56 | - Add CI job to build calculator app #104 57 | - Add support to macOS 12.x and 13.x SDKs via darwin image (osxcross) #133 58 | 59 | ### Changed 60 | 61 | - Bump min Go version to 1.14 to align with Fyne requirements 62 | - Update README for matching modern go command line #114 63 | 64 | ## 1.2.1 - 09 Apr 2022 65 | 66 | ### Added 67 | 68 | - Added the `--engine` flags that allows to specify the container engine to use 69 | between docker and podman. The default behavior is not changed, if the flag is 70 | not specified fyne-cross will auto detect the engine. 71 | 72 | ### Fixed 73 | 74 | - Windows builds no longer pass "-H windowsgui" #97 75 | - Multiple tags cannot be specified using the `-tags` flag #96 76 | 77 | ## 1.2.0 - 07 Mar 2022 78 | 79 | ### Added 80 | 81 | - Add support for FyneApp.toml #78 82 | - Add the ability to use podman #41 83 | - Update to use fixuid to handle mount permissions #42 84 | 85 | ## 1.1.3 - 02 Nov 2021 86 | 87 | ### Fixed 88 | 89 | - Building for windows fails to add icon #66 90 | - Fixes darwin image creation (SDK extraction) #80 91 | 92 | ## 1.1.2 - 05 Oct 2021 93 | 94 | ### Fixed 95 | 96 | - Unsupported target operating system "linux/amd64" #74 97 | 98 | ## 1.1.1 - 29 Sep 2021 99 | 100 | ### Added 101 | 102 | - Support specifying target architectures for Android #52 103 | 104 | ### Changed 105 | 106 | - Switch to x/sys/execabs for windows security fixes #57 107 | - [base-image] update Go to v1.16.8 and Fyne CLI tool to v2.1.0 #67 108 | 109 | ## 1.1.0 - 14 May 2021 110 | 111 | ### Added 112 | 113 | - Add darwin arm64 target #39 114 | - Add FreeBSD on arm64 target #29 115 | - Add the `darwin-image` command to build the darwin docker image 116 | - Add the `local` flag for darwin to build directly from the host 117 | - Add a dedicated docker image for macOS 118 | - Add a dedicated docker image for Windows 119 | - Darwin image build: add support for SDK version #45 120 | 121 | ### Changed 122 | 123 | - Update Go to v1.16.4 124 | - Update fyne CLI to v2.0.3 125 | - Update FreeBSD SDK to v12.2 #29 126 | - Refactor docker images layout to ensure compatibility with previous versions of fyne-cross 127 | 128 | ### Fixed 129 | 130 | - Fix android keystore path is not resolved correctly 131 | - Fix some release flags are always set even if empty 132 | - Fix appID flag should not have a default #25 133 | - Fix the option --env does not allow values containing comma #35 134 | 135 | ### Removed 136 | 137 | - Remove darwin 386 target 138 | - Remove the dependency from the docker/golang-cross image for the base image 139 | 140 | ## 1.0.0 - 13 December 2020 141 | 142 | - Add support for "fyne release" #3 143 | - Add support for creating packaged .tar.gz bundles on freebsd #6 144 | - Add support for Linux Wayland #10 145 | - Update fyne cli to v1.4.2 (fyne-io#1538 fyne-io#1527) 146 | - Deprecate `output` flag in favour of `name` 147 | - Fix env flag validation #14 148 | - Fix build failure for Linux mobile #19 149 | - Update Go to v1.14.13 150 | 151 | ## 0.9.0 - 17 October 2020 152 | 153 | - Releaseing under project namespace with previous 2.2.1 becoming 0.9.0 in fyne-io namespace 154 | 155 | # Archive - lucor/fyne-cross 156 | 157 | ## [2.2.1] - 2020-09-16 158 | 159 | - Fix iOS fails with "only on darwin" when on mac #78 160 | - Update README installation when module-aware mode is not enabled 161 | 162 | ## [2.2.0] - 2020-09-01 163 | 164 | - Add `--pull` option to attempt to pull a newer version of the docker image #75 165 | 166 | ## [2.1.2] - 2020-08-13 167 | 168 | - Update base image to dockercore/golang-cross@1.13.15 (Go v1.13.15) 169 | - fyne cli updated to v1.3.3 170 | 171 | ## [2.1.1] - 2020-07-17 172 | 173 | - Update base image to dockercore/golang-cross@1.13.14 (Go v1.13.14) 174 | 175 | ## [2.1.0] - 2020-07-16 176 | 177 | - Add support for build flags #69 178 | - Base image is based on dockercore/golang-cross@1.13.13 (Go v1.13.13) 179 | - fyne cli updated to v1.3.2 180 | 181 | ## [2.0.0] - 2020-06-07 182 | 183 | - Base image is based on dockercore/golang-cross@1.13.12 (Go v1.13.12) 184 | - fyne cli updated to v1.3.0 185 | 186 | ## [2.0.0-beta4] - 2020-05-21 187 | 188 | - Print fyne cli version in debug mode 189 | - Update unit tests to work on windows 190 | - Fix some minor linter suggestions 191 | - Update docker base image to go v1.13.11 192 | 193 | ## [2.0.0-beta3] - 2020-05-13 194 | 195 | - Remove package option. Package can be now specified as argument 196 | - Fix android build when the package is not into the root dir 197 | 198 | ## [2.0.0-beta2] - 2020-05-13 199 | 200 | - Fix build for packages not in root dir 201 | - Fix ldflags flag not honored #62 202 | 203 | ## [2.0.0-beta1] - 2020-05-10 204 | 205 | - Add subcommand support 206 | - Add a flag to build as "console binary" for Windows #57 207 | - Add support for custom env variables #59 208 | - Add support for custom docker image #52 209 | - Add support for FreeBSD #23 210 | 211 | ## [1.5.0] - 2020-04-13 212 | 213 | - Add android support #37 214 | - Add iOS support on Darwin hosts 215 | - Issue cross compiling from Windows 10 #54 216 | - Update to golang-cross:1.13.10 image (go v1.13.10) 217 | - Update to fyne cli v1.2.4 218 | 219 | ## [1.4.0] - 2020-03-04 220 | 221 | - Add ability to package with an icon using fyne/cmd #14 222 | - Update to golang-cross:1.13.8 image (go v1.13.8) #46 223 | - Disable android build. See #34 224 | - Add support for passing appID to dist packaging #45 225 | - Introduce a root folder and layout for fyne-cross output #38 226 | - Remove OS and Arch info from output #48 227 | - GOCACHE folder is now mounted under $HOME/.cache/fyne-cross/go-build to cache build outputs for reuse in future builds. 228 | 229 | ## [1.3.2] - 2020-01-08 230 | 231 | - Update to golang-cross:1.12.14 image (go v1.12.14) 232 | 233 | ## [1.3.1] - 2019-12-26 234 | 235 | - Default binary name should be folder if none is provided [#29](https://github.com/lucor/fyne-cross/issues/29) 236 | - Cannot build android app when not using go modules [#30](https://github.com/lucor/fyne-cross/issues/30) 237 | 238 | ## [1.3.0] - 2019-11-02 239 | 240 | - Add Android support [#10](https://github.com/lucor/fyne-cross/issues/10) 241 | - GOOS is not set for go get when project do not use go modules [#22](https://github.com/lucor/fyne-cross/issues/22) 242 | - linux/386 does not work with 1.2.x [#24](https://github.com/lucor/fyne-cross/issues/24) 243 | 244 | ## [1.2.2] - 2019-10-29 245 | 246 | - Add wildcard support for goarch [#15](https://github.com/lucor/fyne-cross/issues/15) 247 | - Fix misleading error message when docker daemon is not available [#19](https://github.com/lucor/fyne-cross/issues/19) 248 | - Fix build for windows/386 is failing 249 | 250 | ## [1.2.1] - 2019-10-26 251 | 252 | - Fix fyne-cross docker image build tag 253 | 254 | ## [1.2.0] - 2019-10-26 255 | 256 | - Fix UID is already in use [#12](https://github.com/lucor/fyne-cross/issues/12) 257 | - Update docker image to golang-cross:1.12.12 258 | - Add `--no-strip` flag. Since 1.1.0 by default the -w and -s flags are passed to the linker to strip binaries size omitting the symbol table, debug information and the DWARF symbol table. Specify this flag to add debug info. [#13](https://github.com/lucor/fyne-cross/issues/13) 259 | 260 | ## [1.1.0] - 2019-09-29 261 | 262 | - Added support to `linux/arm` and `linux/arm64` targets 263 | - Updated to golang-cross:1.12.10 image (go v1.12.10 CVE-2019-16276) 264 | 265 | ## [1.0.0] - 2019-09-07 266 | 267 | First stable release 268 | -------------------------------------------------------------------------------- /internal/cloud/s3.go: -------------------------------------------------------------------------------- 1 | package cloud 2 | 3 | import ( 4 | "archive/tar" 5 | "context" 6 | "errors" 7 | "fmt" 8 | "io" 9 | "os" 10 | "path/filepath" 11 | "strings" 12 | "sync" 13 | 14 | "github.com/aws/aws-sdk-go/aws" 15 | "github.com/aws/aws-sdk-go/aws/credentials" 16 | "github.com/aws/aws-sdk-go/aws/session" 17 | "github.com/aws/aws-sdk-go/service/s3" 18 | "github.com/aws/aws-sdk-go/service/s3/s3manager" 19 | "github.com/klauspost/compress/zstd" 20 | "golang.org/x/sync/errgroup" 21 | 22 | "github.com/mholt/archiver/v3" 23 | ) 24 | 25 | type AWSSession struct { 26 | s *session.Session 27 | bucket string 28 | 29 | m sync.Mutex 30 | cancel context.CancelFunc 31 | } 32 | 33 | func Exists(path string) bool { 34 | _, err := os.Stat(path) 35 | if err != nil { 36 | return !errors.Is(err, os.ErrNotExist) 37 | } 38 | return true 39 | } 40 | 41 | func NewAWSSessionFromEnvironment() (*AWSSession, error) { 42 | return NewAWSSession("", "", os.Getenv("AWS_S3_ENDPOINT"), os.Getenv("AWS_S3_REGION"), os.Getenv("AWS_S3_BUCKET")) 43 | } 44 | 45 | func NewAWSSession(akid string, secret string, endpoint string, region string, bucket string) (*AWSSession, error) { 46 | var cred *credentials.Credentials 47 | 48 | if len(bucket) == 0 { 49 | return nil, fmt.Errorf("no bucket specified") 50 | } 51 | 52 | if akid != "" && secret != "" { 53 | cred = credentials.NewStaticCredentials(akid, secret, "") 54 | } 55 | 56 | s, err := session.NewSession( 57 | &aws.Config{ 58 | Endpoint: aws.String(endpoint), 59 | Region: aws.String(region), 60 | Credentials: cred, 61 | }, 62 | ) 63 | if err != nil { 64 | return nil, err 65 | } 66 | 67 | return &AWSSession{s: s, bucket: bucket, cancel: func() {}}, nil 68 | } 69 | 70 | func (a *AWSSession) GetCredentials() (credentials.Value, error) { 71 | a.m.Lock() 72 | ctx, cancel := context.WithCancel(context.Background()) 73 | a.cancel = cancel 74 | a.m.Unlock() 75 | defer a.Cancel() 76 | 77 | return a.s.Config.Credentials.GetWithContext(ctx) 78 | } 79 | 80 | func (a *AWSSession) UploadFile(localFile string, s3FilePath string) error { 81 | file, err := os.Open(localFile) 82 | if err != nil { 83 | return err 84 | } 85 | defer file.Close() 86 | 87 | uploader := s3manager.NewUploader(a.s) 88 | 89 | a.m.Lock() 90 | ctxt, cancel := context.WithCancel(context.Background()) 91 | a.cancel = cancel 92 | a.m.Unlock() 93 | defer a.Cancel() 94 | 95 | _, err = uploader.UploadWithContext(ctxt, &s3manager.UploadInput{ 96 | Bucket: aws.String(a.bucket), 97 | Key: aws.String(s3FilePath), 98 | 99 | Body: file, 100 | }) 101 | 102 | return err 103 | } 104 | 105 | func (a *AWSSession) UploadCompressedDirectory(localDirectoy string, s3FilePath string) error { 106 | file, err := os.CreateTemp("", "fyne-cross-s3") 107 | if err != nil { 108 | return err 109 | } 110 | defer os.Remove(file.Name()) 111 | 112 | extension := strings.ToLower(filepath.Ext(s3FilePath)) 113 | 114 | var compression archiver.Writer 115 | var eg errgroup.Group 116 | var closer io.Closer 117 | 118 | switch extension { 119 | case ".xz": 120 | 121 | compression = archiver.NewTarXz() 122 | err := compression.Create(file) 123 | if err != nil { 124 | return err 125 | } 126 | case ".zstd": 127 | inZstd, outTar := io.Pipe() 128 | closer = outTar 129 | 130 | compression = archiver.NewTar() 131 | err := compression.Create(outTar) 132 | if err != nil { 133 | return err 134 | } 135 | 136 | enc, err := zstd.NewWriter(file) 137 | if err != nil { 138 | return err 139 | } 140 | 141 | eg.Go(func() error { 142 | _, err := io.Copy(enc, inZstd) 143 | if err != nil { 144 | return err 145 | } 146 | 147 | inZstd.Close() 148 | enc.Close() 149 | return nil 150 | }) 151 | default: 152 | return fmt.Errorf("unknown extension for %v", s3FilePath) 153 | } 154 | 155 | base := filepath.Base(localDirectoy) 156 | 157 | err = filepath.Walk(localDirectoy, func(path string, info os.FileInfo, err error) error { 158 | if err != nil { 159 | return err 160 | } 161 | 162 | customName := strings.TrimPrefix(path, localDirectoy) 163 | if customName == path { 164 | return fmt.Errorf("unexpected path: `%v` triming `%v`", path, localDirectoy) 165 | } 166 | customName = filepath.ToSlash(customName) 167 | if len(customName) == 0 || customName[0] != '/' { 168 | customName = "/" + customName 169 | } 170 | customName = base + customName 171 | 172 | if info.IsDir() { 173 | return compression.Write(archiver.File{ 174 | FileInfo: archiver.FileInfo{ 175 | FileInfo: info, 176 | CustomName: customName, 177 | }, 178 | }) 179 | } 180 | 181 | f, err := os.Open(path) 182 | if err != nil { 183 | return err 184 | } 185 | defer f.Close() 186 | 187 | return compression.Write(archiver.File{ 188 | FileInfo: archiver.FileInfo{ 189 | FileInfo: info, 190 | CustomName: customName, 191 | }, 192 | ReadCloser: f, 193 | }) 194 | }) 195 | if err != nil { 196 | return err 197 | } 198 | 199 | compression.Close() 200 | if closer != nil { 201 | closer.Close() 202 | } 203 | if err := eg.Wait(); err != nil { 204 | return err 205 | } 206 | 207 | uploader := s3manager.NewUploader(a.s) 208 | 209 | a.m.Lock() 210 | ctxt, cancel := context.WithCancel(context.Background()) 211 | a.cancel = cancel 212 | a.m.Unlock() 213 | defer a.Cancel() 214 | 215 | f, err := os.Open(file.Name()) 216 | if err != nil { 217 | return err 218 | } 219 | 220 | _, err = uploader.UploadWithContext(ctxt, &s3manager.UploadInput{ 221 | Bucket: aws.String(a.bucket), 222 | Key: aws.String(s3FilePath), 223 | 224 | Body: f, 225 | }) 226 | if err != nil { 227 | return err 228 | } 229 | f.Close() 230 | 231 | return err 232 | } 233 | 234 | func (a *AWSSession) DownloadFile(s3FilePath string, localFile string) error { 235 | f, err := os.Create(localFile) 236 | if err != nil { 237 | return err 238 | } 239 | 240 | downloader := s3manager.NewDownloader(a.s) 241 | 242 | a.m.Lock() 243 | ctxt, cancel := context.WithCancel(context.Background()) 244 | a.cancel = cancel 245 | a.m.Unlock() 246 | defer a.Cancel() 247 | 248 | _, err = downloader.DownloadWithContext(ctxt, f, &s3.GetObjectInput{ 249 | Bucket: aws.String(a.bucket), 250 | Key: aws.String(s3FilePath), 251 | }) 252 | 253 | return err 254 | } 255 | 256 | func (a *AWSSession) DownloadCompressedDirectory(s3FilePath string, localRootDirectory string) error { 257 | file, err := os.CreateTemp("", "fyne-cross-s3") 258 | if err != nil { 259 | return err 260 | } 261 | defer os.Remove(file.Name()) 262 | 263 | a.m.Lock() 264 | ctxt, cancel := context.WithCancel(context.Background()) 265 | a.cancel = cancel 266 | a.m.Unlock() 267 | defer a.Cancel() 268 | 269 | downloader := s3manager.NewDownloader(a.s) 270 | downloader.Concurrency = 1 271 | 272 | _, err = downloader.DownloadWithContext(ctxt, fakeWriterAt{file}, &s3.GetObjectInput{ 273 | Bucket: aws.String(a.bucket), 274 | Key: aws.String(s3FilePath), 275 | }) 276 | file.Close() 277 | if err != nil { 278 | return err 279 | } 280 | 281 | in, err := os.Open(file.Name()) 282 | if err != nil { 283 | return err 284 | } 285 | 286 | extension := strings.ToLower(filepath.Ext(s3FilePath)) 287 | 288 | var compression archiver.Reader 289 | var eg errgroup.Group 290 | 291 | switch extension { 292 | case ".xz": 293 | compression = archiver.NewTarXz() 294 | err := compression.Open(in, 0) 295 | if err != nil { 296 | return err 297 | } 298 | case ".zstd": 299 | inTar, outZstd := io.Pipe() 300 | 301 | dec, err := zstd.NewReader(in) 302 | if err != nil { 303 | return err 304 | } 305 | defer dec.Close() 306 | 307 | eg.Go(func() error { 308 | // Copy content... 309 | _, err := io.Copy(outZstd, dec) 310 | return err 311 | }) 312 | 313 | compression = archiver.NewTar() 314 | err = compression.Open(inTar, 0) 315 | if err != nil { 316 | return err 317 | } 318 | default: 319 | return fmt.Errorf("unknown extension for %v", s3FilePath) 320 | } 321 | 322 | for { 323 | f, err := compression.Read() 324 | if err == io.EOF { 325 | break 326 | } 327 | if err != nil { 328 | return err 329 | } 330 | 331 | err = uncompressFile(localRootDirectory, f) 332 | if err != nil { 333 | return err 334 | } 335 | } 336 | 337 | in.Close() 338 | return eg.Wait() 339 | 340 | } 341 | 342 | func (a *AWSSession) Cancel() { 343 | a.m.Lock() 344 | defer a.m.Unlock() 345 | 346 | a.cancel() 347 | a.cancel = func() {} 348 | } 349 | 350 | func uncompressFile(localRootDirectory string, f archiver.File) error { 351 | // be sure to close f before moving on!! 352 | defer f.Close() 353 | 354 | header := f.Header.(*tar.Header) 355 | 356 | // Do not use strings.Split to split a path as it will generate empty string when given "//" 357 | splitFn := func(c rune) bool { 358 | return c == '/' 359 | } 360 | paths := strings.FieldsFunc(header.Name, splitFn) 361 | if len(paths) == 0 { 362 | if f.Name() != "/" { 363 | return fmt.Errorf("incorrect path") 364 | } 365 | paths = append(paths, "/") 366 | } 367 | 368 | // Replace top directory in the archive with local path 369 | paths[0] = localRootDirectory 370 | localFile := filepath.Join(paths...) 371 | if f.IsDir() { 372 | if !Exists(localFile) { 373 | logWrapper("Creating directory: %s\n", localFile) 374 | return os.MkdirAll(localFile, f.Mode().Perm()) 375 | } 376 | return nil 377 | } 378 | 379 | outFile, err := os.Create(localFile) 380 | if err != nil { 381 | return err 382 | } 383 | defer outFile.Close() 384 | 385 | logWrapper("%s -> %s\n", header.Name, localFile) 386 | _, err = io.Copy(outFile, f) 387 | 388 | return err 389 | } 390 | 391 | func (a *AWSSession) GetBucket() string { 392 | return a.bucket 393 | } 394 | 395 | type fakeWriterAt struct { 396 | w io.Writer 397 | } 398 | 399 | func (fw fakeWriterAt) WriteAt(p []byte, offset int64) (n int, err error) { 400 | // ignore 'offset' because we forced sequential downloads 401 | return fw.w.Write(p) 402 | } 403 | -------------------------------------------------------------------------------- /internal/command/kubernetes.go: -------------------------------------------------------------------------------- 1 | //go:build k8s 2 | // +build k8s 3 | 4 | package command 5 | 6 | import ( 7 | "context" 8 | "crypto/rand" 9 | "flag" 10 | "fmt" 11 | "os" 12 | "os/signal" 13 | "path/filepath" 14 | "strings" 15 | "sync" 16 | "syscall" 17 | 18 | "github.com/fyne-io/fyne-cross/internal/cloud" 19 | "github.com/fyne-io/fyne-cross/internal/log" 20 | "github.com/fyne-io/fyne-cross/internal/volume" 21 | ) 22 | 23 | type kubernetesContainerEngine struct { 24 | baseEngine 25 | 26 | aws *cloud.AWSSession 27 | client *cloud.K8sClient 28 | 29 | mutex sync.Mutex 30 | currentImage *kubernetesContainerImage 31 | 32 | namespace string 33 | s3Path string 34 | storageLimit string 35 | 36 | noProjectUpload bool 37 | noResultDownload bool 38 | } 39 | 40 | var client *cloud.K8sClient 41 | 42 | func kubernetesFlagSet(flagSet *flag.FlagSet, flags *CommonFlags) { 43 | flagSet.BoolVar(&flags.NoProjectUpload, "no-project-upload", false, "Will reuse the project data available in S3, used by the kubernetes engine.") 44 | flagSet.BoolVar(&flags.NoResultDownload, "no-result-download", false, "Will not download the result of the compilation from S3 automatically, used by the kubernetes engine.") 45 | flagSet.StringVar(&flags.Namespace, "namespace", "default", "The namespace the kubernetes engine will use to run the pods in, used by and imply the kubernetes engine.") 46 | flagSet.StringVar(&flags.S3Path, "S3-path", "/", "The path to push to and pull data from, used by the kubernetes engine.") 47 | flagSet.StringVar(&flags.SizeLimit, "size-limit", "2Gi", "The size limit of mounted filesystem inside the container, used by the kubernetes engine.") 48 | } 49 | 50 | func checkKubernetesClient() (err error) { 51 | client, err = cloud.GetKubernetesClient() 52 | return err 53 | } 54 | 55 | func newKubernetesContainerRunner(context Context) (containerEngine, error) { 56 | aws, err := cloud.NewAWSSessionFromEnvironment() 57 | if err != nil { 58 | return nil, err 59 | } 60 | 61 | if client == nil { 62 | return nil, err 63 | } 64 | 65 | engine := &kubernetesContainerEngine{ 66 | baseEngine: baseEngine{ 67 | env: context.Env, 68 | tags: context.Tags, 69 | vol: context.Volume, 70 | }, 71 | namespace: context.Namespace, 72 | s3Path: context.S3Path, 73 | noProjectUpload: context.NoProjectUpload, 74 | noResultDownload: context.NoResultDownload, 75 | aws: aws, 76 | client: client, 77 | storageLimit: context.SizeLimit, 78 | } 79 | 80 | c := make(chan os.Signal, 1) 81 | signal.Notify(c, os.Interrupt, syscall.SIGTERM) 82 | go func() { 83 | <-c 84 | log.Info("Interupting") 85 | engine.close() 86 | os.Exit(1) 87 | }() 88 | 89 | return engine, nil 90 | } 91 | 92 | type kubernetesContainerImage struct { 93 | baseContainerImage 94 | 95 | pod *cloud.Pod 96 | 97 | noProjectUpload bool 98 | 99 | cloudLocalMount []containerMountPoint 100 | 101 | runner *kubernetesContainerEngine 102 | } 103 | 104 | var _ containerEngine = (*kubernetesContainerEngine)(nil) 105 | var _ closer = (*kubernetesContainerImage)(nil) 106 | 107 | func (r *kubernetesContainerEngine) createContainerImage(arch Architecture, OS string, image string) containerImage { 108 | noProjectUpload := r.noProjectUpload 109 | r.noProjectUpload = false // We only need to upload data to S3 from the host once. 110 | 111 | return r.createContainerImageInternal(arch, OS, image, func(base baseContainerImage) containerImage { 112 | ret := &kubernetesContainerImage{ 113 | baseContainerImage: base, 114 | noProjectUpload: noProjectUpload, 115 | runner: r, 116 | } 117 | ret.cloudLocalMount = append(ret.cloudLocalMount, containerMountPoint{name: "cache", inContainer: r.vol.CacheDirContainer()}) 118 | return ret 119 | }) 120 | } 121 | 122 | func (r *kubernetesContainerEngine) close() { 123 | r.mutex.Lock() 124 | currentImage := r.currentImage 125 | r.mutex.Unlock() 126 | 127 | if currentImage != nil { 128 | currentImage.close() 129 | } 130 | } 131 | 132 | func (i *kubernetesContainerImage) Engine() containerEngine { 133 | return i.runner 134 | } 135 | 136 | func (i *kubernetesContainerImage) close() error { 137 | var err error 138 | 139 | defer i.runner.mutex.Unlock() 140 | i.runner.mutex.Lock() 141 | 142 | if i.pod != nil { 143 | err = i.pod.Close() 144 | i.pod = nil 145 | } 146 | if i.runner.currentImage == i { 147 | i.runner.currentImage = nil 148 | } 149 | 150 | return err 151 | } 152 | 153 | func (i *kubernetesContainerImage) Run(vol volume.Volume, opts options, cmdArgs []string) error { 154 | return i.pod.Run(context.Background(), opts.WorkDir, cmdArgs) 155 | } 156 | 157 | func AddAWSParameters(aws *cloud.AWSSession, command string, s ...string) []string { 158 | r := []string{command} 159 | 160 | if endpoint := os.Getenv("AWS_S3_ENDPOINT"); endpoint != "" { 161 | r = append(r, "--aws-endpoint", endpoint) 162 | } 163 | if region := os.Getenv("AWS_S3_REGION"); region != "" { 164 | r = append(r, "--aws-region", region) 165 | } 166 | if bucket := os.Getenv("AWS_S3_BUCKET"); bucket != "" { 167 | r = append(r, "--aws-bucket", bucket) 168 | } 169 | 170 | creds, err := aws.GetCredentials() 171 | if err == nil { 172 | if creds.AccessKeyID != "" { 173 | r = append(r, "--aws-AKID", creds.AccessKeyID) 174 | } 175 | if creds.SecretAccessKey != "" { 176 | r = append(r, "--aws-secret", creds.SecretAccessKey) 177 | } 178 | } else { 179 | log.Infof("Impossible to get AWS credentials.") 180 | } 181 | 182 | return append(r, s...) 183 | } 184 | 185 | func appendKubernetesEnv(env []cloud.Env, environs map[string]string) []cloud.Env { 186 | for k, v := range environs { 187 | env = append(env, cloud.Env{Name: k, Value: v}) 188 | } 189 | return env 190 | } 191 | 192 | func (i *kubernetesContainerImage) Prepare() error { 193 | var err error 194 | 195 | // Upload all mount point to S3 196 | if !i.noProjectUpload { 197 | log.Infof("Uploading project to S3") 198 | for _, mountPoint := range i.mount { 199 | log.Infof("Uploading directory %s compressed to %s.", mountPoint.localHost, i.runner.s3Path+"/"+mountPoint.name+".tar.zstd") 200 | err = i.runner.aws.UploadCompressedDirectory(mountPoint.localHost, i.runner.s3Path+"/"+mountPoint.name+".tar.zstd") 201 | if err != nil { 202 | return err 203 | } 204 | } 205 | } 206 | 207 | // Build pod 208 | var mount []cloud.Mount 209 | 210 | for _, mountPoint := range append(i.mount, i.cloudLocalMount...) { 211 | mount = append(mount, cloud.Mount{ 212 | Name: mountPoint.name, 213 | PathInContainer: mountPoint.inContainer, 214 | }) 215 | } 216 | 217 | cgo := "1" 218 | if i.os == webOS { 219 | cgo = "0" 220 | } 221 | env := []cloud.Env{ 222 | {Name: "CGO_ENABLED", Value: cgo}, // enable CGO 223 | {Name: "GOCACHE", Value: i.runner.vol.GoCacheDirContainer()}, // mount GOCACHE to reuse cache between builds 224 | } 225 | env = appendKubernetesEnv(env, i.runner.env) 226 | env = appendKubernetesEnv(env, i.env) 227 | 228 | // This allow to run more than one fyne-cross for a specific architecture per cluster namespace 229 | var unique [6]byte 230 | rand.Read(unique[:]) 231 | 232 | name := fmt.Sprintf("fyne-cross-%s-%x", i.ID(), unique) 233 | namespace := i.runner.namespace 234 | 235 | i.pod, err = i.runner.client.NewPod(context.Background(), 236 | name, i.DockerImage, namespace, 237 | i.runner.storageLimit, i.runner.vol.WorkDirContainer(), mount, env) 238 | if err != nil { 239 | return err 240 | } 241 | 242 | download := func(vol volume.Volume, downloadPath string, containerPath string) error { 243 | log.Infof("Downloading %s to %s", downloadPath, containerPath) 244 | return i.Run(i.runner.vol, options{}, 245 | AddAWSParameters(i.runner.aws, "fyne-cross-s3", "download-directory", downloadPath, containerPath), 246 | ) 247 | } 248 | 249 | // Download data from S3 for all mount point 250 | log.Infof("Downloading project content from S3 into Kubernetes cluster") 251 | for _, mountPoint := range i.mount { 252 | err = download(i.runner.vol, i.runner.s3Path+"/"+mountPoint.name+".tar.zstd", mountPoint.inContainer) 253 | if err != nil { 254 | return err 255 | } 256 | } 257 | log.Infof("Download cached data if available from S3 into Kubernetes cluster") 258 | for _, mountPoint := range i.cloudLocalMount { 259 | download(i.runner.vol, i.runner.s3Path+"/"+mountPoint.name+"-"+i.ID()+".tar.zstd", mountPoint.inContainer) 260 | } 261 | 262 | log.Infof("Done preparing pods") 263 | 264 | i.runner.currentImage = i 265 | 266 | return nil 267 | } 268 | 269 | func (i *kubernetesContainerImage) Finalize(packageName string) (ret error) { 270 | // Terminate pod on exit 271 | defer func() { 272 | err := i.close() 273 | if err != nil { 274 | ret = err 275 | } 276 | }() 277 | 278 | // golang does use a LIFO for defer, this will be triggered before the i.close() 279 | defer i.runner.mutex.Unlock() 280 | i.runner.mutex.Lock() 281 | 282 | // Upload package result to S3 283 | uploadPath := i.runner.s3Path + "/" + i.ID() + "/" + packageName 284 | log.Infof("Uploading package %s to S3", packageName) 285 | // Darwin application are actually directory and we need 286 | // to compress it in a format that Darwin understand by default 287 | if strings.ToLower(filepath.Ext(packageName)) == ".app" { 288 | uploadPath += ".tar.xz" 289 | ret = i.Run(i.runner.vol, options{}, 290 | AddAWSParameters(i.runner.aws, 291 | "fyne-cross-s3", "upload-directory", 292 | volume.JoinPathContainer(i.runner.vol.TmpDirContainer(), i.ID(), packageName), uploadPath), 293 | ) 294 | } else { 295 | ret = i.Run(i.runner.vol, options{}, 296 | AddAWSParameters(i.runner.aws, 297 | "fyne-cross-s3", "upload-file", 298 | volume.JoinPathContainer(i.runner.vol.TmpDirContainer(), i.ID(), packageName), uploadPath), 299 | ) 300 | } 301 | if ret != nil { 302 | return 303 | } 304 | 305 | // Upload cached data to S3 306 | for _, mountPoint := range i.cloudLocalMount { 307 | log.Infof("Uploading %s to %s", mountPoint.inContainer, i.runner.s3Path+"/"+mountPoint.name+"-"+i.ID()+".tar.zstd") 308 | err := i.Run(i.runner.vol, options{}, 309 | AddAWSParameters(i.runner.aws, 310 | "fyne-cross-s3", "upload-directory", 311 | mountPoint.inContainer, i.runner.s3Path+"/"+mountPoint.name+"-"+i.ID()+".tar.zstd"), 312 | ) 313 | if err != nil { 314 | log.Infof("Failed to upload %s", mountPoint.inContainer) 315 | } 316 | } 317 | 318 | if !i.runner.noResultDownload { 319 | // Download package result from S3 locally 320 | distFile := volume.JoinPathHost(i.runner.vol.DistDirHost(), i.ID(), packageName) 321 | err := os.MkdirAll(filepath.Dir(distFile), 0755) 322 | if err != nil { 323 | ret = fmt.Errorf("could not create the dist package dir: %v", err) 324 | return 325 | } 326 | 327 | log.Infof("Downloading result package to %s.", distFile) 328 | ret = i.runner.aws.DownloadFile(uploadPath, distFile) 329 | 330 | log.Infof("[✓] Package: %q", distFile) 331 | } else { 332 | log.Infof("[✓] Package available at : %q", uploadPath) 333 | } 334 | return 335 | } 336 | -------------------------------------------------------------------------------- /internal/command/command.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path/filepath" 7 | "strings" 8 | "time" 9 | 10 | "github.com/fyne-io/fyne-cross/internal/icon" 11 | "github.com/fyne-io/fyne-cross/internal/log" 12 | "github.com/fyne-io/fyne-cross/internal/volume" 13 | "golang.org/x/sys/execabs" 14 | ) 15 | 16 | // Command wraps the methods for a fyne-cross command 17 | type Command interface { 18 | Name() string // Name returns the one word command name 19 | Description() string // Description returns the command description 20 | Parse(args []string) error // Parse parses the cli arguments 21 | Usage() // Usage displays the command usage 22 | Run() error // Run runs the command 23 | } 24 | 25 | type platformBuilder interface { 26 | Build(image containerImage) (string, error) // Called to build each possible architecture/OS combination 27 | } 28 | 29 | type closer interface { 30 | close() error 31 | } 32 | 33 | func commonRun(defaultContext Context, images []containerImage, builder platformBuilder) error { 34 | for _, image := range images { 35 | log.Infof("[i] Target: %s/%s", image.OS(), image.Architecture()) 36 | log.Debugf("%#v", image) 37 | 38 | err := func() error { 39 | defer image.(closer).close() 40 | 41 | // 42 | // prepare build 43 | // 44 | if err := image.Prepare(); err != nil { 45 | return err 46 | } 47 | 48 | err := cleanTargetDirs(defaultContext, image) 49 | if err != nil { 50 | return err 51 | } 52 | 53 | err = goModInit(defaultContext, image) 54 | if err != nil { 55 | return err 56 | } 57 | 58 | packageName, err := builder.Build(image) 59 | if err != nil { 60 | return err 61 | } 62 | 63 | err = image.Finalize(packageName) 64 | if err != nil { 65 | return err 66 | } 67 | 68 | return nil 69 | }() 70 | 71 | if err != nil { 72 | return err 73 | } 74 | } 75 | 76 | return nil 77 | 78 | } 79 | 80 | // Usage prints the fyne-cross command usage 81 | func Usage(commands []Command) { 82 | template := `fyne-cross is a simple tool to cross compile Fyne applications 83 | 84 | Usage: fyne-cross [arguments] 85 | 86 | The commands are: 87 | 88 | {{ range $k, $cmd := . }} {{ printf "%-13s %s\n" $cmd.Name $cmd.Description }}{{ end }} 89 | Use "fyne-cross -help" for more information about a command. 90 | ` 91 | 92 | printUsage(template, commands) 93 | } 94 | 95 | // cleanTargetDirs cleans the temp dir for the target context 96 | func cleanTargetDirs(ctx Context, image containerImage) error { 97 | 98 | dirs := map[string]string{ 99 | "bin": volume.JoinPathContainer(ctx.BinDirContainer(), image.ID()), 100 | "dist": volume.JoinPathContainer(ctx.DistDirContainer(), image.ID()), 101 | "temp": volume.JoinPathContainer(ctx.TmpDirContainer(), image.ID()), 102 | } 103 | 104 | log.Infof("[i] Cleaning target directories...") 105 | for k, v := range dirs { 106 | err := image.Run(ctx.Volume, options{}, []string{"rm", "-rf", v}) 107 | if err != nil { 108 | return fmt.Errorf("could not clean the %q dir %s: %v", k, v, err) 109 | } 110 | 111 | err = image.Run(ctx.Volume, options{}, []string{"mkdir", "-p", v}) 112 | if err != nil { 113 | return fmt.Errorf("could not create the %q dir %s: %v", k, v, err) 114 | } 115 | 116 | log.Infof("[✓] %q dir cleaned: %s", k, v) 117 | } 118 | 119 | return nil 120 | } 121 | 122 | // prepareIcon prepares the icon for packaging 123 | func prepareIcon(ctx Context, image containerImage) error { 124 | if !ctx.NoProjectUpload { 125 | iconPath := ctx.Icon 126 | if !filepath.IsAbs(ctx.Icon) { 127 | iconPath = volume.JoinPathHost(ctx.WorkDirHost(), ctx.Icon) 128 | } 129 | 130 | if _, err := os.Stat(iconPath); os.IsNotExist(err) { 131 | if ctx.Icon != icon.Default { 132 | return fmt.Errorf("icon not found at %q", ctx.Icon) 133 | } 134 | 135 | log.Infof("[!] Default icon not found at %q", ctx.Icon) 136 | err = os.WriteFile(volume.JoinPathHost(ctx.WorkDirHost(), ctx.Icon), icon.FyneLogo, 0644) 137 | if err != nil { 138 | return fmt.Errorf("could not create the temporary icon: %s", err) 139 | } 140 | log.Infof("[✓] Created a placeholder icon using Fyne logo for testing purpose") 141 | } 142 | } 143 | 144 | err := image.Run(ctx.Volume, options{}, []string{"cp", volume.JoinPathContainer(ctx.WorkDirContainer(), ctx.Icon), volume.JoinPathContainer(ctx.TmpDirContainer(), image.ID(), icon.Default)}) 145 | if err != nil { 146 | return fmt.Errorf("could not copy the icon to temp folder: %v", err) 147 | } 148 | return nil 149 | } 150 | 151 | func printUsage(template string, data interface{}) { 152 | log.PrintTemplate(os.Stderr, template, data) 153 | } 154 | 155 | // checkFyneBinHost checks if the fyne cli tool is installed on the host 156 | func checkFyneBinHost(ctx Context) (string, error) { 157 | fyne, err := execabs.LookPath("fyne") 158 | if err != nil { 159 | return "", fmt.Errorf("missed requirement: fyne. To install: `go install fyne.io/fyne/v2/cmd/fyne@latest` and add $GOPATH/bin to $PATH") 160 | } 161 | 162 | if debugging() { 163 | out, err := execabs.Command(fyne, "version").Output() 164 | if err != nil { 165 | return fyne, fmt.Errorf("could not get fyne cli %s version: %v", fyne, err) 166 | } 167 | log.Debugf("%s", out) 168 | } 169 | 170 | return fyne, nil 171 | } 172 | 173 | func fyneCommand(binary, command, icon string, ctx Context, image containerImage) []string { 174 | target := image.Target() 175 | 176 | args := []string{ 177 | binary, command, 178 | "-os", target, 179 | "-name", ctx.Name, 180 | "-icon", icon, 181 | "-appBuild", ctx.AppBuild, 182 | "-appVersion", ctx.AppVersion, 183 | } 184 | 185 | // add appID to command, if any 186 | if ctx.AppID != "" { 187 | args = append(args, "-appID", ctx.AppID) 188 | } 189 | 190 | // add tags to command, if any 191 | tags := image.Tags() 192 | if len(tags) > 0 { 193 | args = append(args, "-tags", strings.Join(tags, ",")) 194 | } 195 | 196 | if ctx.Metadata != nil { 197 | for key, value := range ctx.Metadata { 198 | args = append(args, "-metadata", fmt.Sprintf("%s=%s", key, value)) 199 | } 200 | } 201 | 202 | return args 203 | } 204 | 205 | // fynePackageHost package the application using the fyne cli tool from the host 206 | // Note: at the moment this is used only for the ios builds 207 | func fynePackageHost(ctx Context, image containerImage) (string, error) { 208 | fyne, err := checkFyneBinHost(ctx) 209 | if err != nil { 210 | return "", err 211 | } 212 | 213 | icon := volume.JoinPathHost(ctx.TmpDirHost(), image.ID(), icon.Default) 214 | args := fyneCommand(fyne, "package", icon, ctx, image) 215 | 216 | // ios packaging require certificate and profile for running on devices 217 | if image.OS() == iosOS { 218 | if ctx.Certificate != "" { 219 | args = append(args, "-certificate", ctx.Certificate) 220 | } 221 | if ctx.Profile != "" { 222 | args = append(args, "-profile", ctx.Profile) 223 | } 224 | } 225 | 226 | workDir := ctx.WorkDirHost() 227 | if image.OS() == iosOS { 228 | workDir = volume.JoinPathHost(workDir, ctx.Package) 229 | } else { 230 | if ctx.Package != "." { 231 | args = append(args, "-src", ctx.Package) 232 | } 233 | } 234 | 235 | // when using local build, do not assume what CC is available and rely on os.Env("CC") is necessary 236 | image.UnsetEnv("CC") 237 | image.UnsetEnv("CGO_CFLAGS") 238 | image.UnsetEnv("CGO_LDFLAGS") 239 | 240 | if ctx.CacheDirHost() != "" { 241 | image.SetEnv("GOCACHE", ctx.CacheDirHost()) 242 | } 243 | 244 | // run the command from the host 245 | fyneCmd := execabs.Command(args[0], args[1:]...) 246 | fyneCmd.Dir = workDir 247 | fyneCmd.Stdout = os.Stdout 248 | fyneCmd.Stderr = os.Stderr 249 | fyneCmd.Env = append(os.Environ(), image.AllEnv()...) 250 | 251 | if debugging() { 252 | log.Debug(fyneCmd) 253 | } 254 | 255 | err = fyneCmd.Run() 256 | if err != nil { 257 | return "", fmt.Errorf("could not package the Fyne app: %v", err) 258 | } 259 | 260 | return searchLocalResult(volume.JoinPathHost(workDir, "*.app")) 261 | } 262 | 263 | // fyneReleaseHost package and release the application using the fyne cli tool from the host 264 | // Note: at the moment this is used only for the ios and windows builds 265 | func fyneReleaseHost(ctx Context, image containerImage) (string, error) { 266 | fyne, err := checkFyneBinHost(ctx) 267 | if err != nil { 268 | return "", err 269 | } 270 | 271 | icon := volume.JoinPathHost(ctx.TmpDirHost(), image.ID(), icon.Default) 272 | args := fyneCommand(fyne, "release", icon, ctx, image) 273 | 274 | workDir := ctx.WorkDirHost() 275 | 276 | ext := "" 277 | switch image.OS() { 278 | case darwinOS: 279 | if ctx.Category != "" { 280 | args = append(args, "-category", ctx.Category) 281 | } 282 | if ctx.Package != "." { 283 | args = append(args, "-src", ctx.Package) 284 | } 285 | ext = ".pkg" 286 | case iosOS: 287 | workDir = volume.JoinPathHost(workDir, ctx.Package) 288 | if ctx.Certificate != "" { 289 | args = append(args, "-certificate", ctx.Certificate) 290 | } 291 | if ctx.Profile != "" { 292 | args = append(args, "-profile", ctx.Profile) 293 | } 294 | ext = ".ipa" 295 | case windowsOS: 296 | if ctx.Certificate != "" { 297 | args = append(args, "-certificate", ctx.Certificate) 298 | } 299 | if ctx.Developer != "" { 300 | args = append(args, "-developer", ctx.Developer) 301 | } 302 | if ctx.Password != "" { 303 | args = append(args, "-password", ctx.Password) 304 | } 305 | if ctx.Package != "." { 306 | args = append(args, "-src", ctx.Package) 307 | } 308 | ext = ".appx" 309 | } 310 | 311 | // when using local build, do not assume what CC is available and rely on os.Env("CC") is necessary 312 | image.UnsetEnv("CC") 313 | image.UnsetEnv("CGO_CFLAGS") 314 | image.UnsetEnv("CGO_LDFLAGS") 315 | 316 | if ctx.CacheDirHost() != "" { 317 | image.SetEnv("GOCACHE", ctx.CacheDirHost()) 318 | } 319 | 320 | // run the command from the host 321 | fyneCmd := execabs.Command(args[0], args[1:]...) 322 | fyneCmd.Dir = workDir 323 | fyneCmd.Stdout = os.Stdout 324 | fyneCmd.Stderr = os.Stderr 325 | fyneCmd.Env = append(os.Environ(), image.AllEnv()...) 326 | 327 | if debugging() { 328 | log.Debug(fyneCmd) 329 | } 330 | 331 | err = fyneCmd.Run() 332 | if err != nil { 333 | return "", fmt.Errorf("could not package the Fyne app: %v", err) 334 | } 335 | return searchLocalResult(volume.JoinPathHost(workDir, "*"+ext)) 336 | } 337 | 338 | func searchLocalResult(path string) (string, error) { 339 | matches, err := filepath.Glob(path) 340 | if err != nil { 341 | return "", fmt.Errorf("could not find the file %v: %v", path, err) 342 | } 343 | 344 | // walk matches files to find the newest file 345 | var newest string 346 | var newestModTime time.Time 347 | for _, match := range matches { 348 | fi, err := os.Stat(match) 349 | if err != nil { 350 | continue 351 | } 352 | 353 | if fi.ModTime().After(newestModTime) { 354 | newest = match 355 | newestModTime = fi.ModTime() 356 | } 357 | } 358 | 359 | if newest == "" { 360 | return "", fmt.Errorf("could not find the file %v", path) 361 | } 362 | return filepath.Base(newest), nil 363 | } 364 | -------------------------------------------------------------------------------- /internal/icon/fyne.go: -------------------------------------------------------------------------------- 1 | /* 2 | The code into this file has been adapted from 3 | https://github.com/fyne-io/fyne/blob/v1.2.3/theme/bundled-icons.go 4 | The original code was released under the following license: 5 | 6 | Copyright (C) 2018 Fyne.io developers (see AUTHORS) 7 | All rights reserved. 8 | 9 | 10 | Redistribution and use in source and binary forms, with or without 11 | modification, are permitted provided that the following conditions are met: 12 | * Redistributions of source code must retain the above copyright 13 | notice, this list of conditions and the following disclaimer. 14 | * Redistributions in binary form must reproduce the above copyright 15 | notice, this list of conditions and the following disclaimer in the 16 | documentation and/or other materials provided with the distribution. 17 | * Neither the name of Fyne.io nor the names of its contributors may be 18 | used to endorse or promote products derived from this software without 19 | specific prior written permission. 20 | 21 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 22 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 23 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 24 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY 25 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 26 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 27 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 28 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 29 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 30 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 31 | */ 32 | 33 | package icon 34 | 35 | // FyneLogo is the Fyne logo in png format 36 | var FyneLogo = []byte{137, 80, 78, 71, 13, 10, 26, 10, 0, 0, 0, 13, 73, 72, 68, 82, 0, 0, 1, 0, 0, 0, 1, 0, 8, 0, 0, 0, 0, 121, 25, 247, 186, 0, 0, 0, 9, 112, 72, 89, 115, 0, 0, 1, 147, 0, 0, 1, 147, 1, 140, 78, 202, 19, 0, 0, 0, 25, 116, 69, 88, 116, 83, 111, 102, 116, 119, 97, 114, 101, 0, 119, 119, 119, 46, 105, 110, 107, 115, 99, 97, 112, 101, 46, 111, 114, 103, 155, 238, 60, 26, 0, 0, 8, 212, 73, 68, 65, 84, 120, 218, 237, 221, 253, 87, 20, 85, 24, 7, 112, 254, 180, 186, 179, 176, 139, 11, 235, 75, 234, 154, 10, 145, 120, 80, 148, 208, 34, 197, 151, 76, 142, 128, 133, 40, 69, 162, 29, 80, 219, 2, 73, 201, 52, 9, 18, 216, 3, 150, 102, 161, 212, 154, 39, 76, 225, 144, 22, 66, 224, 11, 138, 130, 8, 184, 46, 147, 56, 251, 50, 15, 9, 115, 119, 187, 187, 247, 238, 125, 230, 254, 202, 238, 89, 158, 207, 188, 220, 153, 239, 220, 123, 39, 129, 32, 111, 9, 38, 128, 9, 96, 2, 152, 0, 38, 128, 9, 96, 2, 152, 0, 38, 128, 9, 96, 2, 152, 0, 38, 128, 9, 16, 47, 45, 113, 249, 59, 123, 170, 207, 252, 240, 179, 231, 90, 207, 192, 192, 173, 238, 78, 207, 143, 205, 213, 229, 239, 101, 47, 177, 32, 0, 88, 185, 235, 152, 103, 240, 153, 250, 210, 118, 46, 89, 114, 0, 103, 89, 235, 144, 58, 107, 155, 58, 170, 72, 125, 8, 100, 84, 247, 168, 115, 181, 167, 187, 101, 62, 7, 56, 42, 186, 212, 185, 219, 240, 91, 18, 159, 4, 179, 206, 76, 24, 148, 175, 222, 92, 46, 111, 47, 144, 219, 62, 101, 84, 190, 218, 145, 42, 109, 55, 184, 198, 163, 26, 183, 19, 22, 34, 41, 192, 210, 86, 227, 173, 175, 122, 203, 100, 189, 16, 82, 202, 198, 40, 54, 255, 72, 158, 172, 87, 130, 111, 118, 235, 55, 243, 192, 111, 77, 199, 62, 175, 42, 47, 171, 116, 29, 107, 185, 114, 199, 23, 252, 67, 95, 186, 164, 151, 194, 202, 167, 147, 129, 26, 7, 154, 203, 50, 103, 28, 229, 73, 217, 229, 205, 125, 211, 127, 243, 204, 151, 244, 94, 96, 193, 37, 127, 245, 119, 191, 90, 59, 235, 101, 241, 254, 142, 211, 73, 146, 222, 12, 101, 223, 214, 202, 255, 187, 216, 18, 213, 223, 17, 21, 160, 64, 187, 242, 25, 218, 165, 68, 249, 135, 4, 5, 168, 212, 58, 191, 214, 249, 81, 255, 37, 33, 1, 148, 227, 218, 121, 191, 4, 105, 32, 162, 212, 191, 168, 255, 73, 62, 210, 68, 72, 249, 246, 69, 253, 147, 185, 4, 41, 128, 182, 255, 171, 165, 4, 41, 128, 75, 171, 191, 142, 32, 5, 40, 212, 206, 255, 125, 54, 164, 0, 27, 189, 90, 188, 247, 54, 193, 9, 224, 28, 214, 14, 0, 55, 193, 9, 144, 212, 169, 213, 239, 75, 71, 10, 112, 210, 127, 255, 211, 70, 112, 2, 108, 242, 167, 63, 83, 153, 56, 1, 22, 4, 30, 122, 92, 38, 56, 1, 220, 129, 252, 99, 55, 78, 128, 188, 64, 252, 57, 150, 140, 18, 32, 121, 48, 176, 3, 52, 16, 148, 0, 174, 96, 202, 153, 135, 18, 96, 233, 120, 160, 254, 199, 73, 40, 1, 154, 130, 59, 64, 27, 193, 8, 176, 204, 27, 4, 40, 68, 9, 208, 16, 172, 255, 217, 124, 140, 0, 75, 130, 207, 64, 212, 46, 130, 17, 224, 100, 232, 17, 88, 29, 70, 128, 215, 116, 227, 31, 182, 97, 4, 168, 209, 141, 116, 114, 32, 4, 176, 220, 13, 1, 244, 16, 132, 0, 91, 117, 79, 193, 235, 49, 2, 156, 211, 1, 148, 34, 4, 88, 240, 84, 7, 144, 133, 16, 224, 160, 126, 28, 72, 18, 66, 128, 155, 58, 128, 110, 130, 15, 32, 77, 63, 224, 169, 17, 33, 128, 254, 8, 80, 43, 228, 1, 40, 161, 221, 152, 96, 32, 228, 38, 105, 0, 74, 220, 23, 233, 62, 104, 215, 247, 1, 170, 83, 22, 128, 162, 54, 7, 37, 64, 129, 190, 254, 49, 69, 18, 128, 162, 182, 212, 20, 74, 128, 38, 61, 192, 117, 34, 7, 192, 243, 250, 237, 148, 0, 10, 152, 2, 210, 34, 7, 192, 116, 253, 180, 0, 43, 193, 176, 223, 35, 82, 0, 188, 168, 159, 22, 160, 24, 0, 20, 203, 0, 160, 213, 79, 11, 112, 10, 0, 228, 72, 0, 224, 175, 159, 22, 224, 58, 0, 88, 28, 255, 0, 129, 250, 41, 1, 172, 224, 42, 96, 66, 137, 123, 128, 96, 253, 148, 0, 57, 96, 7, 232, 53, 234, 50, 94, 17, 29, 160, 196, 157, 98, 15, 0, 220, 58, 68, 209, 218, 1, 128, 209, 87, 142, 172, 23, 28, 64, 87, 191, 221, 190, 157, 166, 253, 10, 0, 46, 26, 124, 186, 246, 218, 74, 161, 1, 66, 251, 63, 117, 187, 10, 0, 92, 6, 159, 46, 186, 49, 186, 82, 96, 128, 8, 234, 183, 63, 0, 0, 37, 134, 0, 42, 107, 129, 4, 190, 245, 47, 134, 243, 191, 242, 140, 1, 88, 11, 36, 112, 173, 223, 158, 11, 1, 210, 41, 0, 24, 11, 36, 68, 229, 252, 71, 221, 74, 224, 4, 200, 84, 26, 0, 182, 2, 204, 0, 26, 63, 137, 160, 126, 123, 13, 0, 24, 180, 83, 1, 48, 21, 96, 6, 160, 212, 31, 136, 0, 160, 13, 0, 252, 78, 9, 192, 82, 128, 221, 57, 32, 34, 129, 63, 0, 64, 43, 45, 0, 67, 1, 134, 189, 64, 36, 2, 247, 0, 192, 151, 212, 0, 236, 4, 88, 94, 7, 132, 47, 224, 128, 139, 161, 148, 211, 3, 168, 35, 43, 4, 188, 18, 12, 91, 96, 21, 236, 5, 183, 27, 2, 12, 171, 172, 5, 216, 222, 11, 40, 141, 161, 141, 152, 50, 58, 52, 107, 123, 240, 80, 107, 163, 51, 230, 193, 63, 52, 106, 62, 221, 167, 217, 28, 5, 140, 239, 6, 117, 251, 64, 74, 183, 26, 229, 198, 68, 128, 117, 30, 16, 18, 136, 62, 0, 19, 1, 230, 137, 80, 80, 32, 6, 0, 44, 4, 216, 103, 130, 1, 129, 88, 0, 48, 16, 136, 66, 42, 236, 23, 136, 9, 128, 250, 40, 241, 85, 241, 158, 11, 104, 2, 49, 1, 240, 109, 78, 18, 111, 15, 240, 11, 196, 2, 224, 255, 215, 31, 165, 103, 131, 211, 2, 49, 0, 96, 80, 127, 180, 158, 14, 63, 23, 136, 62, 0, 139, 250, 163, 54, 62, 64, 169, 63, 216, 29, 15, 245, 71, 111, 132, 136, 210, 208, 213, 111, 212, 30, 195, 245, 0, 251, 141, 219, 132, 190, 126, 43, 17, 25, 128, 144, 215, 29, 70, 173, 3, 46, 8, 103, 248, 121, 135, 53, 244, 24, 193, 183, 137, 205, 136, 66, 174, 163, 196, 254, 4, 0, 167, 141, 191, 16, 2, 96, 179, 255, 243, 6, 24, 1, 0, 85, 97, 0, 48, 171, 159, 43, 128, 29, 158, 212, 138, 232, 1, 216, 213, 207, 21, 96, 70, 28, 146, 75, 13, 192, 176, 126, 174, 0, 91, 32, 128, 147, 22, 128, 101, 253, 92, 1, 202, 64, 253, 207, 18, 41, 1, 152, 214, 207, 21, 224, 40, 0, 184, 77, 232, 0, 216, 214, 207, 21, 192, 13, 159, 138, 208, 1, 48, 174, 159, 43, 192, 21, 248, 84, 132, 10, 128, 117, 253, 92, 1, 6, 1, 64, 45, 13, 64, 7, 235, 250, 121, 2, 40, 94, 0, 240, 17, 13, 128, 211, 74, 228, 1, 88, 10, 123, 193, 173, 20, 95, 73, 100, 63, 163, 136, 35, 0, 28, 33, 167, 102, 242, 249, 47, 56, 2, 236, 130, 0, 41, 232, 0, 42, 97, 192, 77, 208, 1, 192, 97, 210, 55, 240, 1, 92, 0, 0, 237, 248, 0, 122, 194, 141, 67, 100, 3, 120, 4, 0, 14, 161, 3, 72, 86, 57, 207, 21, 225, 13, 144, 17, 118, 28, 34, 25, 192, 230, 176, 227, 16, 201, 0, 246, 134, 29, 135, 72, 6, 80, 29, 118, 28, 34, 25, 64, 115, 216, 113, 136, 100, 0, 151, 195, 142, 67, 36, 3, 232, 15, 59, 14, 145, 11, 64, 153, 4, 0, 31, 163, 3, 152, 49, 87, 100, 27, 58, 128, 117, 66, 196, 33, 28, 1, 10, 32, 64, 42, 58, 0, 176, 116, 138, 58, 70, 208, 1, 124, 13, 223, 22, 136, 15, 224, 188, 16, 113, 8, 71, 128, 46, 33, 226, 16, 142, 0, 195, 66, 196, 33, 252, 0, 108, 83, 66, 196, 33, 252, 0, 210, 197, 136, 67, 248, 1, 188, 11, 1, 150, 161, 3, 40, 133, 131, 62, 19, 209, 1, 124, 1, 0, 238, 16, 116, 0, 103, 0, 64, 39, 62, 0, 143, 24, 113, 8, 63, 128, 62, 56, 103, 22, 29, 128, 50, 33, 70, 28, 194, 13, 96, 145, 32, 113, 8, 55, 128, 108, 65, 226, 16, 110, 0, 59, 5, 137, 67, 184, 1, 28, 16, 36, 14, 225, 6, 112, 66, 144, 56, 132, 27, 192, 57, 65, 226, 16, 110, 0, 112, 21, 193, 122, 124, 0, 247, 1, 192, 97, 116, 0, 86, 81, 226, 16, 94, 0, 105, 176, 23, 220, 128, 14, 32, 79, 148, 56, 132, 23, 64, 137, 40, 113, 8, 47, 0, 151, 40, 113, 8, 47, 128, 239, 68, 137, 67, 120, 1, 252, 34, 74, 28, 194, 11, 160, 87, 148, 56, 132, 23, 192, 56, 92, 66, 12, 29, 192, 66, 97, 226, 16, 78, 0, 107, 32, 192, 106, 116, 0, 59, 32, 128, 3, 29, 64, 133, 48, 113, 8, 39, 128, 58, 97, 226, 16, 78, 0, 223, 195, 5, 197, 241, 1, 192, 165, 84, 79, 225, 3, 0, 47, 150, 137, 253, 219, 181, 184, 3, 36, 249, 196, 185, 12, 224, 2, 176, 2, 246, 130, 25, 232, 0, 96, 28, 50, 101, 69, 7, 176, 7, 46, 40, 78, 208, 1, 192, 209, 33, 30, 124, 0, 45, 0, 160, 1, 31, 0, 92, 59, 164, 18, 31, 0, 92, 59, 228, 125, 116, 0, 22, 175, 56, 55, 195, 92, 0, 150, 193, 25, 147, 54, 116, 0, 27, 5, 186, 23, 228, 2, 240, 129, 56, 145, 48, 31, 0, 151, 72, 157, 0, 15, 0, 248, 84, 36, 31, 31, 0, 28, 36, 186, 8, 31, 0, 152, 51, 59, 64, 208, 1, 192, 57, 179, 109, 248, 0, 150, 8, 20, 7, 113, 1, 200, 225, 252, 154, 89, 238, 0, 69, 224, 45, 171, 73, 248, 0, 14, 131, 229, 180, 9, 62, 128, 6, 145, 46, 131, 120, 0, 128, 215, 172, 102, 35, 4, 184, 173, 171, 127, 216, 130, 15, 0, 140, 145, 108, 38, 248, 0, 50, 245, 71, 64, 1, 66, 0, 253, 84, 9, 111, 10, 66, 128, 26, 65, 134, 201, 115, 3, 184, 164, 3, 40, 196, 8, 160, 27, 40, 63, 62, 15, 33, 128, 83, 183, 3, 184, 9, 66, 128, 66, 145, 110, 132, 120, 0, 232, 242, 176, 30, 130, 17, 64, 247, 84, 168, 20, 35, 128, 110, 170, 200, 80, 50, 70, 128, 10, 33, 38, 76, 115, 4, 184, 26, 154, 37, 97, 197, 8, 224, 156, 18, 98, 166, 24, 63, 128, 208, 148, 225, 78, 5, 35, 128, 242, 87, 112, 158, 84, 22, 193, 8, 16, 26, 30, 118, 156, 160, 4, 8, 46, 168, 223, 107, 67, 9, 144, 22, 24, 33, 234, 93, 75, 80, 2, 180, 5, 118, 128, 3, 4, 37, 64, 102, 160, 15, 188, 160, 224, 4, 8, 68, 33, 189, 41, 4, 37, 64, 113, 32, 10, 79, 35, 40, 1, 22, 250, 151, 144, 156, 88, 79, 80, 2, 40, 254, 69, 84, 189, 249, 4, 39, 128, 255, 145, 168, 119, 7, 193, 9, 176, 69, 235, 1, 188, 59, 9, 78, 128, 220, 39, 218, 241, 47, 220, 246, 143, 17, 64, 238, 152, 246, 58, 177, 13, 4, 39, 192, 251, 218, 246, 191, 151, 73, 80, 2, 40, 85, 218, 241, 223, 227, 36, 40, 1, 210, 252, 239, 18, 113, 219, 8, 70, 0, 155, 75, 27, 21, 56, 89, 65, 8, 66, 0, 123, 197, 61, 255, 229, 127, 22, 145, 20, 96, 71, 199, 225, 220, 89, 70, 186, 41, 27, 235, 71, 252, 51, 3, 79, 39, 19, 57, 1, 44, 181, 211, 231, 183, 241, 203, 117, 197, 171, 102, 172, 133, 148, 81, 218, 124, 55, 112, 247, 255, 96, 59, 33, 114, 2, 44, 212, 13, 248, 242, 14, 94, 105, 57, 238, 170, 44, 219, 95, 85, 123, 242, 167, 155, 186, 69, 82, 124, 223, 56, 136, 164, 0, 107, 224, 236, 175, 151, 183, 238, 117, 132, 72, 10, 176, 239, 169, 113, 249, 247, 247, 42, 68, 82, 0, 107, 163, 113, 249, 195, 85, 201, 132, 72, 10, 224, 188, 46, 71, 249, 145, 2, 228, 63, 50, 42, 191, 243, 67, 43, 33, 178, 2, 40, 85, 190, 185, 171, 31, 109, 88, 77, 226, 165, 69, 0, 224, 104, 159, 123, 215, 111, 218, 105, 35, 68, 98, 128, 212, 174, 217, 139, 247, 245, 212, 230, 40, 36, 174, 90, 4, 123, 128, 37, 231, 51, 207, 216, 127, 139, 255, 231, 236, 193, 13, 243, 72, 220, 181, 8, 123, 1, 101, 213, 238, 154, 179, 157, 55, 6, 250, 187, 59, 61, 231, 221, 53, 251, 54, 191, 97, 35, 241, 217, 18, 8, 242, 102, 2, 152, 0, 38, 128, 9, 96, 2, 152, 0, 38, 128, 9, 96, 2, 152, 0, 38, 128, 9, 96, 2, 96, 108, 255, 2, 197, 33, 52, 81, 15, 83, 174, 161, 0, 0, 0, 0, 73, 69, 78, 68, 174, 66, 96, 130} 37 | --------------------------------------------------------------------------------