/
build.go
247 lines (214 loc) · 7.25 KB
/
build.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
// Copyright 2015 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:generate go run gendex.go -o dex.go
package mobile
import (
"bufio"
"errors"
"fmt"
"io"
"regexp"
"strconv"
"strings"
"slices"
"log/slog"
"maps"
"cogentcore.org/core/cmd/core/config"
"cogentcore.org/core/exec"
"cogentcore.org/core/logx"
"golang.org/x/tools/go/packages"
)
var TmpDir string
// Build compiles and encodes the app named by the import path.
//
// The named package must define a main function.
//
// The -target flag takes either android (the default), or one or more
// comma-delimited Apple platforms (TODO: apple platforms list).
//
// For -target android, if an AndroidManifest.xml is defined in the
// package directory, it is added to the APK output. Otherwise, a default
// manifest is generated. By default, this builds a fat APK for all supported
// instruction sets (arm, 386, amd64, arm64). A subset of instruction sets can
// be selected by specifying target type with the architecture name. E.g.
// -target=android/arm,android/386.
//
// For Apple -target platforms, gomobile must be run on an OS X machine with
// Xcode installed.
//
// By default, -target ios will generate an XCFramework for both ios
// and iossimulator. Multiple Apple targets can be specified, creating a "fat"
// XCFramework with each slice. To generate a fat XCFramework that supports
// iOS, macOS, and macCatalyst for all supportec architectures (amd64 and arm64),
// specify -target ios,macos,maccatalyst. A subset of instruction sets can be
// selectged by specifying the platform with an architecture name. E.g.
// -target=ios/arm64,maccatalyst/arm64.
//
// If the package directory contains an assets subdirectory, its contents
// are copied into the output.
func Build(c *config.Config) error {
_, err := BuildImpl(c)
return err
}
// BuildImpl builds a package for mobiles based on the given config info.
// BuildImpl returns a built package information and an error if exists.
func BuildImpl(c *config.Config) (*packages.Package, error) {
cleanup, err := BuildEnvInit(c)
if err != nil {
return nil, err
}
defer cleanup()
for _, platform := range c.Build.Target {
if platform.Arch == "*" {
archs := config.ArchsForOS[platform.OS]
c.Build.Target = make([]config.Platform, len(archs))
for i, arch := range archs {
c.Build.Target[i] = config.Platform{OS: platform.OS, Arch: arch}
}
}
}
// Special case to add iossimulator if we don't already have it and we have ios
hasIOSSimulator := slices.ContainsFunc(c.Build.Target,
func(p config.Platform) bool { return p.OS == "iossimulator" })
hasIOS := slices.ContainsFunc(c.Build.Target,
func(p config.Platform) bool { return p.OS == "ios" })
if !hasIOSSimulator && hasIOS {
c.Build.Target = append(c.Build.Target, config.Platform{OS: "iossimulator", Arch: "arm64"}) // TODO: set arch better here
}
// TODO(ydnar): this should work, unless build tags affect loading a single package.
// Should we try to import packages with different build tags per platform?
pkgs, err := packages.Load(PackagesConfig(c, &c.Build.Target[0]), ".")
if err != nil {
return nil, err
}
// len(pkgs) can be more than 1 e.g., when the specified path includes `...`.
if len(pkgs) != 1 {
return nil, fmt.Errorf("expected 1 package but got %d", len(pkgs))
}
pkg := pkgs[0]
if pkg.Name != "main" {
return nil, fmt.Errorf("cannot build non-main package")
}
if c.ID == "" {
return nil, fmt.Errorf("id must be set when building for mobile")
}
switch {
case IsAndroidPlatform(c.Build.Target[0].OS):
if pkg.Name != "main" {
for _, t := range c.Build.Target {
if err := GoBuild(c, pkg.PkgPath, AndroidEnv[t.Arch]); err != nil {
return nil, err
}
}
return pkg, nil
}
_, err = GoAndroidBuild(c, pkg, c.Build.Target)
if err != nil {
return nil, err
}
case IsApplePlatform(c.Build.Target[0].OS):
if !XCodeAvailable() {
return nil, fmt.Errorf("-target=%s requires XCode", c.Build.Target)
}
if pkg.Name != "main" {
for _, t := range c.Build.Target {
// Catalyst support requires iOS 13+
v, _ := strconv.ParseFloat(c.Build.IOSVersion, 64)
if t.OS == "maccatalyst" && v < 13.0 {
return nil, errors.New("catalyst requires -iosversion=13 or higher")
}
if err := GoBuild(c, pkg.PkgPath, AppleEnv[t.String()]); err != nil {
return nil, err
}
}
return pkg, nil
}
_, err = GoAppleBuild(c, pkg, c.Build.Target)
if err != nil {
return nil, err
}
}
return pkg, nil
}
var NmRE = regexp.MustCompile(`[0-9a-f]{8} t _?(?:.*/vendor/)?(golang.org/x.*/[^.]*)`)
func ExtractPkgs(c *config.Config, nm string, path string) (map[string]bool, error) {
r, w := io.Pipe()
nmpkgs := make(map[string]bool)
errc := make(chan error, 1)
go func() {
s := bufio.NewScanner(r)
for s.Scan() {
if res := NmRE.FindStringSubmatch(s.Text()); res != nil {
nmpkgs[res[1]] = true
}
}
errc <- s.Err()
}()
err := exec.Major().SetStdout(w).Run(nm, path)
w.Close()
if err != nil {
return nil, fmt.Errorf("%s %s: %v", nm, path, err)
}
if err := <-errc; err != nil {
return nil, fmt.Errorf("%s %s: %v", nm, path, err)
}
return nmpkgs, nil
}
func GoBuild(c *config.Config, src string, env map[string]string, args ...string) error {
return GoCmd(c, "build", []string{src}, env, args...)
}
func GoBuildAt(c *config.Config, at string, src string, env map[string]string, args ...string) error {
return GoCmdAt(c, at, "build", []string{src}, env, args...)
}
func GoInstall(c *config.Config, srcs []string, env map[string]string, args ...string) error {
return GoCmd(c, "install", srcs, env, args...)
}
func GoCmd(c *config.Config, subcmd string, srcs []string, env map[string]string, args ...string) error {
return GoCmdAt(c, "", subcmd, srcs, env, args...)
}
func GoCmdAt(c *config.Config, at string, subcmd string, srcs []string, env map[string]string, args ...string) error {
cargs := []string{subcmd}
// cmd := exec.Command("go", subcmd)
var tags []string
if c.Build.Debug {
tags = append(tags, "debug")
}
if len(tags) > 0 {
cargs = append(cargs, "-tags", strings.Join(tags, ","))
}
if logx.UserLevel <= slog.LevelInfo {
cargs = append(cargs, "-v")
}
cargs = append(cargs, args...)
cargs = append(cargs, srcs...)
xc := exec.Major().SetDir(at)
maps.Copy(xc.Env, env)
// Specify GOMODCACHE explicitly. The default cache path is GOPATH[0]/pkg/mod,
// but the path varies when GOPATH is specified at env, which results in cold cache.
if gmc, err := GoModCachePath(); err == nil {
xc.SetEnv("GOMODCACHE", gmc)
}
return xc.Run("go", cargs...)
}
func GoModTidyAt(c *config.Config, at string, env map[string]string) error {
args := []string{"mod", "tidy"}
if logx.UserLevel <= slog.LevelInfo {
args = append(args, "-v")
}
xc := exec.Major().SetDir(at)
maps.Copy(xc.Env, env)
// Specify GOMODCACHE explicitly. The default cache path is GOPATH[0]/pkg/mod,
// but the path varies when GOPATH is specified at env, which results in cold cache.
if gmc, err := GoModCachePath(); err == nil {
xc.SetEnv("GOMODCACHE", gmc)
}
return exec.Run("go", args...)
}
func GoModCachePath() (string, error) {
out, err := exec.Output("go", "env", "GOMODCACHE")
if err != nil {
return "", err
}
return strings.TrimSpace(string(out)), nil
}