├── 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 | [](https://github.com/fyne-io/fyne-cross/actions?query=workflow%3ACI) [](https://goreportcard.com/report/github.com/fyne-io/fyne-cross) [](http://godoc.org/github.com/fyne-io/fyne-cross) []()
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 |
--------------------------------------------------------------------------------