1
0
Fork 0

Adding upstream version 0.0~git20250520.a1d9079+dfsg.

Signed-off-by: Daniel Baumann <daniel@debian.org>
This commit is contained in:
Daniel Baumann 2025-05-24 19:46:29 +02:00
parent 590ac7ff5f
commit 20149b7f3a
Signed by: daniel
GPG key ID: FBB4F0E80A80222F
456 changed files with 70406 additions and 0 deletions

252
cmd/gobind/doc.go Normal file
View file

@ -0,0 +1,252 @@
// Copyright 2014 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.
/*
Gobind generates language bindings that make it possible to call Go
functions from Java and Objective-C.
Typically gobind is not used directly. Instead, a binding is
generated and automatically packaged for Android or iOS by
`gomobile bind`. For more details on installing and using the gomobile
tool, see https://golang.org/x/mobile/cmd/gomobile.
# Binding Go
Gobind generates target language (Java or Objective-C) bindings for
each exported symbol in a Go package. The Go package you choose to
bind defines a cross-language interface.
Bindings require additional Go code be generated, so using gobind
manually requires calling it twice, first with -lang=<target>, where
target is either java or objc, and again with -lang=go. The generated
package can then be _ imported into a Go program, typically built
with -buildmode=c-archive for iOS or -buildmode=c-shared for Android.
These details are handled by the `gomobile bind` command.
# Passing Go objects to target languages
Consider a type for counting:
package mypkg
type Counter struct {
Value int
}
func (c *Counter) Inc() { c.Value++ }
func NewCounter() *Counter { return &Counter{ 5 } }
In Java, the generated bindings are,
public abstract class Mypkg {
public static native Counter newCounter();
}
and
public final class Counter {
public Counter() { ... }
public final long getValue();
public final void setValue(long v);
public void inc();
}
The package-level function newCounter can be called like so:
Counter c = Mypkg.newCounter()
For convenience, functions on the form NewT(...) *T are converted to constructors for T:
Counter c = new Counter()
Both forms returns a Java Counter, which is a proxy for a Go *Counter. Calling the inc, getValue and
setValue methods will call the Go implementations of these methods.
Similarly, the same Go package will generate the Objective-C interface
@class GoMypkgCounter;
@interface GoMypkgCounter : NSObject {
}
@property(strong, readonly) id ref;
- (void)inc;
- (int64_t)value;
- (void)setValue:(int64_t)v;
@end
FOUNDATION_EXPORT GoMypkgCounter* GoMypkgNewCounter(void);
The equivalent of calling newCounter in Go is GoMypkgNewCounter in Objective-C.
The returned GoMypkgCounter* holds a reference to an underlying Go
*Counter.
# Passing target language objects to Go
For a Go interface:
package myfmt
type Printer interface {
Print(s string)
}
func PrintHello(p Printer) {
p.Print("Hello, World!")
}
gobind generates a Java interface that can be used to implement a Printer:
public abstract class Myfmt {
public static void printHello(Printer p0);
}
and
public interface Printer {
public void print(String s);
}
You can implement Printer, and pass it to Go using the printHello
package function:
public class SysPrint implements Printer {
public void print(String s) {
System.out.println(s);
}
}
The Java implementation can be used like so:
Printer printer = new SysPrint();
Myfmt.printHello(printer);
For Objective-C binding, gobind generates a protocol that declares
methods corresponding to Go interface's methods.
@protocol GoMyfmtPrinter
- (void)Print:(NSString*)s;
@end
FOUNDATION_EXPORT void GoMyfmtPrintHello(id<GoMyfmtPrinter> p0);
Any Objective-C classes conforming to the GoMyfmtPrinter protocol can be
passed to Go using the GoMyfmtPrintHello function:
@interface SysPrint : NSObject<GoMyfmtPrinter> {
}
@end
@implementation SysPrint {
}
- (void)Print:(NSString*)s {
NSLog("%@", s);
}
@end
The Objective-C implementation can be used like so:
SysPrint* printer = [[SysPrint alloc] init];
GoMyfmtPrintHello(printer);
# Type restrictions
At present, only a subset of Go types are supported.
All exported symbols in the package must have types that are supported.
Supported types include:
- Signed integer and floating point types.
- String and boolean types.
- Byte slice types. Note that byte slices are passed by reference,
and support mutation.
- Any function type all of whose parameters and results have
supported types. Functions must return either no results,
one result, or two results where the type of the second is
the built-in 'error' type.
- Any interface type, all of whose exported methods have
supported function types.
- Any struct type, all of whose exported methods have
supported function types and all of whose exported fields
have supported types.
Unexported symbols have no effect on the cross-language interface, and
as such are not restricted.
The set of supported types will eventually be expanded to cover more
Go types, but this is a work in progress.
Exceptions and panics are not yet supported. If either pass a language
boundary, the program will exit.
# Reverse bindings
Gobind also supports accessing API from Java or Objective C from Go.
Similar to how Cgo supports the magic "C" import, gobind recognizes
import statements that start with "Java/" or "ObjC/". For example,
to import java.lang.System and call the static method currentTimeMillis:
import "Java/java/lang/System"
t := System.CurrentTimeMillis()
Similarly, to import NSDate and call the static method [NSDate date]:
import "ObjC/Foundation/NSDate"
d := NSDate.Date()
Gobind also supports specifying particular classes, interfaces or
protocols a particular Go struct should extend or implement. For example,
to create an Android Activity subclass MainActivity:
import "Java/android/app/Activity"
type MainActivity struct {
app.Activity
}
Gobind also recognizes Java interfaces as well as Objective C classes and
protocols the same way.
For more details on binding the native API, see the design proposals,
https://golang.org/issues/16876 (Java) and https://golang.org/issues/17102
(Objective C).
# Avoid reference cycles
The language bindings maintain a reference to each object that has been
proxied. When a proxy object becomes unreachable, its finalizer reports
this fact to the object's native side, so that the reference can be
removed, potentially allowing the object to be reclaimed by its native
garbage collector. The mechanism is symmetric.
However, it is possible to create a reference cycle between Go and
objects in target languages, via proxies, meaning objects cannot be
collected. This causes a memory leak.
For example, in Java: if a Go object G holds a reference to the Go
proxy of a Java object J, and J holds a reference to the Java proxy
of G, then the language bindings on each side must keep G and J live
even if they are otherwise unreachable.
We recommend that implementations of foreign interfaces do not hold
references to proxies of objects. That is: if you implement a Go
interface in Java, do not store an instance of Seq.Object inside it.
# Further reading
Examples can be found in http://golang.org/x/mobile/example.
Design doc: http://golang.org/s/gobind
*/
package main

391
cmd/gobind/gen.go Normal file
View file

@ -0,0 +1,391 @@
// Copyright 2014 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.
package main
import (
"bytes"
"fmt"
"go/ast"
"go/token"
"go/types"
"io"
"os"
"path/filepath"
"strings"
"unicode"
"unicode/utf8"
"golang.org/x/mobile/bind"
"golang.org/x/mobile/internal/importers"
"golang.org/x/mobile/internal/importers/java"
"golang.org/x/mobile/internal/importers/objc"
"golang.org/x/tools/go/packages"
)
func genPkg(lang string, p *types.Package, astFiles []*ast.File, allPkg []*types.Package, classes []*java.Class, otypes []*objc.Named) {
fname := defaultFileName(lang, p)
conf := &bind.GeneratorConfig{
Fset: fset,
Pkg: p,
AllPkg: allPkg,
}
var pname string
if p != nil {
pname = p.Name()
} else {
pname = "universe"
}
var buf bytes.Buffer
generator := &bind.Generator{
Printer: &bind.Printer{Buf: &buf, IndentEach: []byte("\t")},
Fset: conf.Fset,
AllPkg: conf.AllPkg,
Pkg: conf.Pkg,
Files: astFiles,
}
switch lang {
case "java":
g := &bind.JavaGen{
JavaPkg: *javaPkg,
Generator: generator,
}
g.Init(classes)
pkgname := bind.JavaPkgName(*javaPkg, p)
pkgDir := strings.Replace(pkgname, ".", "/", -1)
buf.Reset()
w, closer := writer(filepath.Join("java", pkgDir, fname))
processErr(g.GenJava())
io.Copy(w, &buf)
closer()
for i, name := range g.ClassNames() {
buf.Reset()
w, closer := writer(filepath.Join("java", pkgDir, name+".java"))
processErr(g.GenClass(i))
io.Copy(w, &buf)
closer()
}
buf.Reset()
w, closer = writer(filepath.Join("src", "gobind", pname+"_android.c"))
processErr(g.GenC())
io.Copy(w, &buf)
closer()
buf.Reset()
w, closer = writer(filepath.Join("src", "gobind", pname+"_android.h"))
processErr(g.GenH())
io.Copy(w, &buf)
closer()
// Generate support files along with the universe package
if p == nil {
dir, err := packageDir("golang.org/x/mobile/bind")
if err != nil {
errorf(`"golang.org/x/mobile/bind" is not found; run go get golang.org/x/mobile/bind: %v`, err)
return
}
repo := filepath.Clean(filepath.Join(dir, "..")) // golang.org/x/mobile directory.
for _, javaFile := range []string{"Seq.java"} {
src := filepath.Join(repo, "bind/java/"+javaFile)
in, err := os.Open(src)
if err != nil {
errorf("failed to open Java support file: %v", err)
}
defer in.Close()
w, closer := writer(filepath.Join("java", "go", javaFile))
defer closer()
if _, err := io.Copy(w, in); err != nil {
errorf("failed to copy Java support file: %v", err)
return
}
}
// Copy support files
if err != nil {
errorf("unable to import bind/java: %v", err)
return
}
javaDir, err := packageDir("golang.org/x/mobile/bind/java")
if err != nil {
errorf("unable to import bind/java: %v", err)
return
}
copyFile(filepath.Join("src", "gobind", "seq_android.c"), filepath.Join(javaDir, "seq_android.c.support"))
copyFile(filepath.Join("src", "gobind", "seq_android.go"), filepath.Join(javaDir, "seq_android.go.support"))
copyFile(filepath.Join("src", "gobind", "seq_android.h"), filepath.Join(javaDir, "seq_android.h"))
}
case "go":
w, closer := writer(filepath.Join("src", "gobind", fname))
conf.Writer = w
processErr(bind.GenGo(conf))
closer()
w, closer = writer(filepath.Join("src", "gobind", pname+".h"))
genPkgH(w, pname)
io.Copy(w, &buf)
closer()
w, closer = writer(filepath.Join("src", "gobind", "seq.h"))
genPkgH(w, "seq")
io.Copy(w, &buf)
closer()
dir, err := packageDir("golang.org/x/mobile/bind")
if err != nil {
errorf("unable to import bind: %v", err)
return
}
copyFile(filepath.Join("src", "gobind", "seq.go"), filepath.Join(dir, "seq.go.support"))
case "objc":
g := &bind.ObjcGen{
Generator: generator,
Prefix: *prefix,
}
g.Init(otypes)
w, closer := writer(filepath.Join("src", "gobind", pname+"_darwin.h"))
processErr(g.GenGoH())
io.Copy(w, &buf)
closer()
hname := strings.Title(fname[:len(fname)-2]) + ".objc.h"
w, closer = writer(filepath.Join("src", "gobind", hname))
processErr(g.GenH())
io.Copy(w, &buf)
closer()
mname := strings.Title(fname[:len(fname)-2]) + "_darwin.m"
w, closer = writer(filepath.Join("src", "gobind", mname))
conf.Writer = w
processErr(g.GenM())
io.Copy(w, &buf)
closer()
if p == nil {
// Copy support files
dir, err := packageDir("golang.org/x/mobile/bind/objc")
if err != nil {
errorf("unable to import bind/objc: %v", err)
return
}
copyFile(filepath.Join("src", "gobind", "seq_darwin.m"), filepath.Join(dir, "seq_darwin.m.support"))
copyFile(filepath.Join("src", "gobind", "seq_darwin.go"), filepath.Join(dir, "seq_darwin.go.support"))
copyFile(filepath.Join("src", "gobind", "ref.h"), filepath.Join(dir, "ref.h"))
copyFile(filepath.Join("src", "gobind", "seq_darwin.h"), filepath.Join(dir, "seq_darwin.h"))
}
default:
errorf("unknown target language: %q", lang)
}
}
func genPkgH(w io.Writer, pname string) {
fmt.Fprintf(w, `// Code generated by gobind. DO NOT EDIT.
#ifdef __GOBIND_ANDROID__
#include "%[1]s_android.h"
#endif
#ifdef __GOBIND_DARWIN__
#include "%[1]s_darwin.h"
#endif`, pname)
}
func genObjcPackages(dir string, types []*objc.Named, embedders []importers.Struct) error {
var buf bytes.Buffer
cg := &bind.ObjcWrapper{
Printer: &bind.Printer{
IndentEach: []byte("\t"),
Buf: &buf,
},
}
var genNames []string
for _, emb := range embedders {
genNames = append(genNames, emb.Name)
}
cg.Init(types, genNames)
for i, opkg := range cg.Packages() {
pkgDir := filepath.Join(dir, "src", "ObjC", opkg)
if err := os.MkdirAll(pkgDir, 0700); err != nil {
return err
}
pkgFile := filepath.Join(pkgDir, "package.go")
buf.Reset()
cg.GenPackage(i)
if err := os.WriteFile(pkgFile, buf.Bytes(), 0600); err != nil {
return err
}
}
buf.Reset()
cg.GenInterfaces()
objcBase := filepath.Join(dir, "src", "ObjC")
if err := os.MkdirAll(objcBase, 0700); err != nil {
return err
}
if err := os.WriteFile(filepath.Join(objcBase, "interfaces.go"), buf.Bytes(), 0600); err != nil {
return err
}
goBase := filepath.Join(dir, "src", "gobind")
if err := os.MkdirAll(goBase, 0700); err != nil {
return err
}
buf.Reset()
cg.GenGo()
if err := os.WriteFile(filepath.Join(goBase, "interfaces_darwin.go"), buf.Bytes(), 0600); err != nil {
return err
}
buf.Reset()
cg.GenH()
if err := os.WriteFile(filepath.Join(goBase, "interfaces.h"), buf.Bytes(), 0600); err != nil {
return err
}
buf.Reset()
cg.GenM()
if err := os.WriteFile(filepath.Join(goBase, "interfaces_darwin.m"), buf.Bytes(), 0600); err != nil {
return err
}
return nil
}
func genJavaPackages(dir string, classes []*java.Class, embedders []importers.Struct) error {
var buf bytes.Buffer
cg := &bind.ClassGen{
JavaPkg: *javaPkg,
Printer: &bind.Printer{
IndentEach: []byte("\t"),
Buf: &buf,
},
}
cg.Init(classes, embedders)
for i, jpkg := range cg.Packages() {
pkgDir := filepath.Join(dir, "src", "Java", jpkg)
if err := os.MkdirAll(pkgDir, 0700); err != nil {
return err
}
pkgFile := filepath.Join(pkgDir, "package.go")
buf.Reset()
cg.GenPackage(i)
if err := os.WriteFile(pkgFile, buf.Bytes(), 0600); err != nil {
return err
}
}
buf.Reset()
cg.GenInterfaces()
javaBase := filepath.Join(dir, "src", "Java")
if err := os.MkdirAll(javaBase, 0700); err != nil {
return err
}
if err := os.WriteFile(filepath.Join(javaBase, "interfaces.go"), buf.Bytes(), 0600); err != nil {
return err
}
goBase := filepath.Join(dir, "src", "gobind")
if err := os.MkdirAll(goBase, 0700); err != nil {
return err
}
buf.Reset()
cg.GenGo()
if err := os.WriteFile(filepath.Join(goBase, "classes_android.go"), buf.Bytes(), 0600); err != nil {
return err
}
buf.Reset()
cg.GenH()
if err := os.WriteFile(filepath.Join(goBase, "classes.h"), buf.Bytes(), 0600); err != nil {
return err
}
buf.Reset()
cg.GenC()
if err := os.WriteFile(filepath.Join(goBase, "classes_android.c"), buf.Bytes(), 0600); err != nil {
return err
}
return nil
}
func processErr(err error) {
if err != nil {
if list, _ := err.(bind.ErrorList); len(list) > 0 {
for _, err := range list {
errorf("%v", err)
}
} else {
errorf("%v", err)
}
}
}
var fset = token.NewFileSet()
func writer(fname string) (w io.Writer, closer func()) {
if *outdir == "" {
return os.Stdout, func() { return }
}
name := filepath.Join(*outdir, fname)
dir := filepath.Dir(name)
if err := os.MkdirAll(dir, 0755); err != nil {
errorf("invalid output dir: %v", err)
os.Exit(exitStatus)
}
f, err := os.Create(name)
if err != nil {
errorf("invalid output dir: %v", err)
os.Exit(exitStatus)
}
closer = func() {
if err := f.Close(); err != nil {
errorf("error in closing output file: %v", err)
}
}
return f, closer
}
func copyFile(dst, src string) {
w, closer := writer(dst)
f, err := os.Open(src)
if err != nil {
errorf("unable to open file: %v", err)
closer()
os.Exit(exitStatus)
}
if _, err := io.Copy(w, f); err != nil {
errorf("unable to copy file: %v", err)
f.Close()
closer()
os.Exit(exitStatus)
}
f.Close()
closer()
}
func defaultFileName(lang string, pkg *types.Package) string {
switch lang {
case "java":
if pkg == nil {
return "Universe.java"
}
firstRune, size := utf8.DecodeRuneInString(pkg.Name())
className := string(unicode.ToUpper(firstRune)) + pkg.Name()[size:]
return className + ".java"
case "go":
if pkg == nil {
return "go_main.go"
}
return "go_" + pkg.Name() + "main.go"
case "objc":
if pkg == nil {
return "Universe.m"
}
firstRune, size := utf8.DecodeRuneInString(pkg.Name())
className := string(unicode.ToUpper(firstRune)) + pkg.Name()[size:]
return *prefix + className + ".m"
}
errorf("unknown target language: %q", lang)
os.Exit(exitStatus)
return ""
}
func packageDir(path string) (string, error) {
mode := packages.NeedFiles
pkgs, err := packages.Load(&packages.Config{Mode: mode}, path)
if err != nil {
return "", err
}
if len(pkgs) == 0 || len(pkgs[0].GoFiles) == 0 {
return "", fmt.Errorf("no Go package in %v", path)
}
pkg := pkgs[0]
if len(pkg.Errors) > 0 {
return "", fmt.Errorf("%v", pkg.Errors)
}
return filepath.Dir(pkg.GoFiles[0]), nil
}

218
cmd/gobind/gobind_test.go Normal file
View file

@ -0,0 +1,218 @@
// Copyright 2016 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.
package main
import (
"bytes"
"fmt"
"os"
"os/exec"
"runtime"
"strings"
"testing"
"golang.org/x/tools/go/packages/packagestest"
)
func TestMain(m *testing.M) {
// To avoid recompiling the gobind command (and to support compiler options
// like -race and -coverage), allow the test binary itself to re-exec itself
// as the gobind command by setting an environment variable.
if os.Getenv("GOBIND_TEST_IS_GOBIND") != "" {
main()
os.Exit(0)
}
os.Setenv("GOBIND_TEST_IS_GOBIND", "1")
os.Exit(m.Run())
}
var tests = []struct {
name string
lang string
pkg string
goos string
// reverse is true if the test needs to generate reverse bindings using
// external tools such as javap.
reverse bool
}{
{
name: "ObjC-Testpkg",
lang: "objc",
pkg: "golang.org/x/mobile/bind/testdata/testpkg",
},
{
name: "Java-Testpkg",
lang: "java",
pkg: "golang.org/x/mobile/bind/testdata/testpkg",
},
{
name: "Go-Testpkg",
lang: "go",
pkg: "golang.org/x/mobile/bind/testdata/testpkg",
},
{
name: "Java-Javapkg",
lang: "java",
pkg: "golang.org/x/mobile/bind/testdata/testpkg/javapkg",
goos: "android",
reverse: true,
},
{
name: "Go-Javapkg",
lang: "go",
pkg: "golang.org/x/mobile/bind/testdata/testpkg/javapkg",
goos: "android",
reverse: true,
},
{
name: "Go-Cgopkg",
lang: "go,java,objc",
pkg: "golang.org/x/mobile/bind/testdata/cgopkg",
goos: "android",
},
}
func mustHaveBindTestdata(t testing.TB) {
switch runtime.GOOS {
case "android", "ios":
t.Skipf("skipping: test cannot access ../../bind/testdata on %s/%s", runtime.GOOS, runtime.GOARCH)
}
}
func gobindBin(t testing.TB) string {
switch runtime.GOOS {
case "js", "ios":
t.Skipf("skipping: cannot exec subprocess on %s/%s", runtime.GOOS, runtime.GOARCH)
}
p, err := os.Executable()
if err != nil {
t.Fatal(err)
}
return p
}
func runGobind(t testing.TB, lang, pkg, goos string, exported *packagestest.Exported) error {
cmd := exec.Command(gobindBin(t), "-lang", lang, pkg)
cmd.Dir = exported.Config.Dir
cmd.Env = exported.Config.Env
if goos != "" {
// Add CGO_ENABLED=1 explicitly since Cgo is disabled when GOOS is different from host OS.
cmd.Env = append(cmd.Env, "GOOS="+goos, "CGO_ENABLED=1")
}
stderr := new(strings.Builder)
cmd.Stderr = stderr
stdout := new(strings.Builder)
cmd.Stdout = stdout
err := cmd.Run()
if testing.Verbose() && stdout.Len() > 0 {
t.Logf("stdout (%v):\n%s", cmd, stderr)
}
if stderr.Len() > 0 {
t.Logf("stderr (%v):\n%s", cmd, stderr)
}
if err != nil {
return fmt.Errorf("%v: %w", cmd, err)
}
return nil
}
func TestGobind(t *testing.T) { packagestest.TestAll(t, testGobind) }
func testGobind(t *testing.T, exporter packagestest.Exporter) {
mustHaveBindTestdata(t)
_, javapErr := exec.LookPath("javap")
exported := packagestest.Export(t, exporter, []packagestest.Module{{
Name: "golang.org/x/mobile",
Files: packagestest.MustCopyFileTree("../.."),
}})
defer exported.Cleanup()
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
if exporter == packagestest.Modules && test.reverse {
t.Skip("reverse binding does't work with Go modules")
}
if test.reverse && javapErr != nil {
t.Skip("reverse bind test requires javap which is not available")
}
if err := runGobind(t, test.lang, test.pkg, test.goos, exported); err != nil {
t.Error(err)
}
})
}
}
func TestDocs(t *testing.T) { packagestest.TestAll(t, testDocs) }
func testDocs(t *testing.T, exporter packagestest.Exporter) {
mustHaveBindTestdata(t)
const docsrc = `
package doctest
// This is a comment.
type Struct struct{
}`
exported := packagestest.Export(t, exporter, []packagestest.Module{
{
Name: "example.com/doctest",
Files: map[string]interface{}{
"doc.go": docsrc,
},
},
{
// gobind requires golang.org/x/mobile to generate code for reverse bindings.
Name: "golang.org/x/mobile",
Files: packagestest.MustCopyFileTree("../.."),
},
})
defer exported.Cleanup()
const comment = "This is a comment."
for _, lang := range []string{"java", "objc"} {
cmd := exec.Command(gobindBin(t), "-lang", lang, "example.com/doctest")
cmd.Dir = exported.Config.Dir
cmd.Env = exported.Config.Env
out, err := cmd.CombinedOutput()
if err != nil {
t.Errorf("gobind -lang %s failed: %v: %s", lang, err, out)
continue
}
if bytes.Index(out, []byte(comment)) == -1 {
t.Errorf("gobind output for language %s did not contain the comment %q", lang, comment)
}
}
}
func BenchmarkGobind(b *testing.B) {
packagestest.BenchmarkAll(b, benchmarkGobind)
}
func benchmarkGobind(b *testing.B, exporter packagestest.Exporter) {
_, javapErr := exec.LookPath("javap")
exported := packagestest.Export(b, exporter, []packagestest.Module{{
Name: "golang.org/x/mobile",
Files: packagestest.MustCopyFileTree("../.."),
}})
defer exported.Cleanup()
for _, test := range tests {
b.Run(test.name, func(b *testing.B) {
if exporter == packagestest.Modules && test.reverse {
b.Skip("reverse binding does't work with Go modules")
}
if test.reverse && javapErr != nil {
b.Skip("reverse bind test requires javap which is not available")
}
for i := 0; i < b.N; i++ {
if err := runGobind(b, test.lang, test.pkg, test.goos, exported); err != nil {
b.Error(err)
}
}
})
}
}

11
cmd/gobind/implicit.go Normal file
View file

@ -0,0 +1,11 @@
// This file imports implicit dependencies required by generated code.
//go:build mobile_implicit
package main
import (
_ "golang.org/x/mobile/bind"
_ "golang.org/x/mobile/bind/java"
_ "golang.org/x/mobile/bind/objc"
)

168
cmd/gobind/main.go Normal file
View file

@ -0,0 +1,168 @@
// Copyright 2014 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.
package main
import (
"bytes"
"flag"
"fmt"
"go/ast"
"go/types"
"log"
"os"
"os/exec"
"path/filepath"
"strings"
"golang.org/x/mobile/internal/importers"
"golang.org/x/mobile/internal/importers/java"
"golang.org/x/mobile/internal/importers/objc"
"golang.org/x/tools/go/packages"
)
var (
lang = flag.String("lang", "", "target languages for bindings, either java, go, or objc. If empty, all languages are generated.")
outdir = flag.String("outdir", "", "result will be written to the directory instead of stdout.")
javaPkg = flag.String("javapkg", "", "custom Java package path prefix. Valid only with -lang=java.")
prefix = flag.String("prefix", "", "custom Objective-C name prefix. Valid only with -lang=objc.")
bootclasspath = flag.String("bootclasspath", "", "Java bootstrap classpath.")
classpath = flag.String("classpath", "", "Java classpath.")
tags = flag.String("tags", "", "build tags.")
)
var usage = `The Gobind tool generates Java language bindings for Go.
For usage details, see doc.go.`
func main() {
flag.Parse()
run()
os.Exit(exitStatus)
}
func run() {
var langs []string
if *lang != "" {
langs = strings.Split(*lang, ",")
} else {
langs = []string{"go", "java", "objc"}
}
// We need to give appropriate environment variables like CC or CXX so that the returned packages no longer have errors.
// However, getting such environment variables is difficult or impossible so far.
// Gomobile can obtain such environment variables in env.go, but this logic assumes some condiitons gobind doesn't assume.
cfg := &packages.Config{
Mode: packages.NeedName | packages.NeedFiles |
packages.NeedImports | packages.NeedDeps |
packages.NeedTypes | packages.NeedSyntax | packages.NeedTypesInfo,
BuildFlags: []string{"-tags", strings.Join(strings.Split(*tags, ","), " ")},
}
// Call Load twice to warm the cache. There is a known issue that the result of Load
// depends on build cache state. See golang/go#33687.
packages.Load(cfg, flag.Args()...)
allPkg, err := packages.Load(cfg, flag.Args()...)
if err != nil {
log.Fatal(err)
}
jrefs, err := importers.AnalyzePackages(allPkg, "Java/")
if err != nil {
log.Fatal(err)
}
orefs, err := importers.AnalyzePackages(allPkg, "ObjC/")
if err != nil {
log.Fatal(err)
}
var classes []*java.Class
if len(jrefs.Refs) > 0 {
jimp := &java.Importer{
Bootclasspath: *bootclasspath,
Classpath: *classpath,
JavaPkg: *javaPkg,
}
classes, err = jimp.Import(jrefs)
if err != nil {
log.Fatal(err)
}
}
var otypes []*objc.Named
if len(orefs.Refs) > 0 {
otypes, err = objc.Import(orefs)
if err != nil {
log.Fatal(err)
}
}
if len(classes) > 0 || len(otypes) > 0 {
srcDir := *outdir
if srcDir == "" {
srcDir, err = os.MkdirTemp(os.TempDir(), "gobind-")
if err != nil {
log.Fatal(err)
}
defer os.RemoveAll(srcDir)
} else {
srcDir, err = filepath.Abs(srcDir)
if err != nil {
log.Fatal(err)
}
}
if len(classes) > 0 {
if err := genJavaPackages(srcDir, classes, jrefs.Embedders); err != nil {
log.Fatal(err)
}
}
if len(otypes) > 0 {
if err := genObjcPackages(srcDir, otypes, orefs.Embedders); err != nil {
log.Fatal(err)
}
}
// Add a new directory to GOPATH where the file for reverse bindings exist, and recreate allPkg.
// It is because the current allPkg did not solve imports for reverse bindings.
var gopath string
if out, err := exec.Command("go", "env", "GOPATH").Output(); err != nil {
log.Fatal(err)
} else {
gopath = string(bytes.TrimSpace(out))
}
if gopath != "" {
gopath = string(filepath.ListSeparator) + gopath
}
gopath = srcDir + gopath
cfg.Env = append(os.Environ(), "GOPATH="+gopath)
allPkg, err = packages.Load(cfg, flag.Args()...)
if err != nil {
log.Fatal(err)
}
}
typePkgs := make([]*types.Package, len(allPkg))
astPkgs := make([][]*ast.File, len(allPkg))
for i, pkg := range allPkg {
// Ignore pkg.Errors. pkg.Errors can exist when Cgo is used, but this should not affect the result.
// See the discussion at golang/go#36547.
typePkgs[i] = pkg.Types
astPkgs[i] = pkg.Syntax
}
for _, l := range langs {
for i, pkg := range typePkgs {
genPkg(l, pkg, astPkgs[i], typePkgs, classes, otypes)
}
// Generate the error package and support files
genPkg(l, nil, nil, typePkgs, classes, otypes)
}
}
var exitStatus = 0
func errorf(format string, args ...interface{}) {
fmt.Fprintf(os.Stderr, format, args...)
fmt.Fprintln(os.Stderr)
exitStatus = 1
}

355
cmd/gomobile/bind.go Normal file
View file

@ -0,0 +1,355 @@
// 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.
package main
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"os"
"os/exec"
"path/filepath"
"strings"
"sync"
"golang.org/x/mobile/internal/sdkpath"
"golang.org/x/mod/modfile"
"golang.org/x/tools/go/packages"
)
var cmdBind = &command{
run: runBind,
Name: "bind",
Usage: "[-target android|" + strings.Join(applePlatforms, "|") + "] [-bootclasspath <path>] [-classpath <path>] [-o output] [build flags] [package]",
Short: "build a library for Android and iOS",
Long: `
Bind generates language bindings for the package named by the import
path, and compiles a library for the named target system.
The -target flag takes either android (the default), or one or more
comma-delimited Apple platforms (` + strings.Join(applePlatforms, ", ") + `).
For -target android, the bind command produces an AAR (Android ARchive)
file that archives the precompiled Java API stub classes, the compiled
shared libraries, and all asset files in the /assets subdirectory under
the package directory. The output is named '<package_name>.aar' by
default. This AAR file is commonly used for binary distribution of an
Android library project and most Android IDEs support AAR import. For
example, in Android Studio (1.2+), an AAR file can be imported using
the module import wizard (File > New > New Module > Import .JAR or
.AAR package), and setting it as a new dependency
(File > Project Structure > Dependencies). This requires 'javac'
(version 1.8+) and Android SDK (API level 16 or newer) to build the
library for Android. The ANDROID_HOME and ANDROID_NDK_HOME environment
variables can be used to specify the Android SDK and NDK if they are
not in the default locations. Use the -javapkg flag to specify the Java
package prefix for the generated classes.
By default, -target=android builds shared libraries for all supported
instruction sets (arm, arm64, 386, amd64). 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. The generated Objective-C types can be prefixed with the
-prefix flag.
For -target android, the -bootclasspath and -classpath flags are used to
control the bootstrap classpath and the classpath for Go wrappers to Java
classes.
The -v flag provides verbose output, including the list of packages built.
The build flags -a, -n, -x, -gcflags, -ldflags, -tags, -trimpath, and -work
are shared with the build command. For documentation, see 'go help build'.
`,
}
func runBind(cmd *command) error {
cleanup, err := buildEnvInit()
if err != nil {
return err
}
defer cleanup()
args := cmd.flag.Args()
targets, err := parseBuildTarget(buildTarget)
if err != nil {
return fmt.Errorf(`invalid -target=%q: %v`, buildTarget, err)
}
if isAndroidPlatform(targets[0].platform) {
if bindPrefix != "" {
return fmt.Errorf("-prefix is supported only for Apple targets")
}
if _, err := ndkRoot(targets[0]); err != nil {
return err
}
} else {
if bindJavaPkg != "" {
return fmt.Errorf("-javapkg is supported only for android target")
}
}
var gobind string
if !buildN {
gobind, err = exec.LookPath("gobind")
if err != nil {
return errors.New("gobind was not found. Please run gomobile init before trying again")
}
} else {
gobind = "gobind"
}
if len(args) == 0 {
args = append(args, ".")
}
// 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(targets[0]), args...)
if err != nil {
return err
}
// check if any of the package is main
for _, pkg := range pkgs {
if pkg.Name == "main" {
return fmt.Errorf(`binding "main" package (%s) is not supported`, pkg.PkgPath)
}
}
switch {
case isAndroidPlatform(targets[0].platform):
return goAndroidBind(gobind, pkgs, targets)
case isApplePlatform(targets[0].platform):
if !xcodeAvailable() {
return fmt.Errorf("-target=%q requires Xcode", buildTarget)
}
return goAppleBind(gobind, pkgs, targets)
default:
return fmt.Errorf(`invalid -target=%q`, buildTarget)
}
}
var (
bindPrefix string // -prefix
bindJavaPkg string // -javapkg
bindClasspath string // -classpath
bindBootClasspath string // -bootclasspath
)
func init() {
// bind command specific commands.
cmdBind.flag.StringVar(&bindJavaPkg, "javapkg", "",
"specifies custom Java package path prefix. Valid only with -target=android.")
cmdBind.flag.StringVar(&bindPrefix, "prefix", "",
"custom Objective-C name prefix. Valid only with -target=ios.")
cmdBind.flag.StringVar(&bindClasspath, "classpath", "", "The classpath for imported Java classes. Valid only with -target=android.")
cmdBind.flag.StringVar(&bindBootClasspath, "bootclasspath", "", "The bootstrap classpath for imported Java classes. Valid only with -target=android.")
}
func bootClasspath() (string, error) {
if bindBootClasspath != "" {
return bindBootClasspath, nil
}
apiPath, err := sdkpath.AndroidAPIPath(buildAndroidAPI)
if err != nil {
return "", err
}
return filepath.Join(apiPath, "android.jar"), nil
}
func copyFile(dst, src string) error {
if buildX {
printcmd("cp %s %s", src, dst)
}
return writeFile(dst, func(w io.Writer) error {
if buildN {
return nil
}
f, err := os.Open(src)
if err != nil {
return err
}
defer f.Close()
if _, err := io.Copy(w, f); err != nil {
return fmt.Errorf("cp %s %s failed: %v", src, dst, err)
}
return nil
})
}
func writeFile(filename string, generate func(io.Writer) error) error {
if buildV {
fmt.Fprintf(os.Stderr, "write %s\n", filename)
}
if err := mkdir(filepath.Dir(filename)); err != nil {
return err
}
if buildN {
return generate(io.Discard)
}
f, err := os.Create(filename)
if err != nil {
return err
}
defer func() {
if cerr := f.Close(); err == nil {
err = cerr
}
}()
return generate(f)
}
func packagesConfig(t targetInfo) *packages.Config {
config := &packages.Config{}
// Add CGO_ENABLED=1 explicitly since Cgo is disabled when GOOS is different from host OS.
config.Env = append(os.Environ(), "GOARCH="+t.arch, "GOOS="+platformOS(t.platform), "CGO_ENABLED=1")
tags := append(buildTags[:], platformTags(t.platform)...)
if len(tags) > 0 {
config.BuildFlags = []string{"-tags=" + strings.Join(tags, ",")}
}
return config
}
// getModuleVersions returns a module information at the directory src.
func getModuleVersions(targetPlatform string, targetArch string, src string) (*modfile.File, error) {
cmd := exec.Command("go", "list")
cmd.Env = append(os.Environ(), "GOOS="+platformOS(targetPlatform), "GOARCH="+targetArch)
tags := append(buildTags[:], platformTags(targetPlatform)...)
// TODO(hyangah): probably we don't need to add all the dependencies.
cmd.Args = append(cmd.Args, "-m", "-json", "-tags="+strings.Join(tags, ","), "all")
cmd.Dir = src
output, err := cmd.Output()
if err != nil {
// Module information is not available at src.
return nil, nil
}
type Module struct {
Main bool
Path string
Version string
Dir string
Replace *Module
}
f := &modfile.File{}
if err := f.AddModuleStmt("gobind"); err != nil {
return nil, err
}
e := json.NewDecoder(bytes.NewReader(output))
for {
var mod *Module
err := e.Decode(&mod)
if err != nil && err != io.EOF {
return nil, err
}
if mod != nil {
if mod.Replace != nil {
p, v := mod.Replace.Path, mod.Replace.Version
if modfile.IsDirectoryPath(p) {
// replaced by a local directory
p = mod.Replace.Dir
}
if err := f.AddReplace(mod.Path, mod.Version, p, v); err != nil {
return nil, err
}
} else {
// When the version part is empty, the module is local and mod.Dir represents the location.
if v := mod.Version; v == "" {
if err := f.AddReplace(mod.Path, mod.Version, mod.Dir, ""); err != nil {
return nil, err
}
} else {
if err := f.AddRequire(mod.Path, v); err != nil {
return nil, err
}
}
}
}
if err == io.EOF {
break
}
}
v, err := ensureGoVersion()
if err != nil {
return nil, err
}
// ensureGoVersion can return an empty string for a devel version. In this case, use the minimum version.
if v == "" {
v = fmt.Sprintf("go1.%d", minimumGoMinorVersion)
}
if err := f.AddGoStmt(strings.TrimPrefix(v, "go")); err != nil {
return nil, err
}
return f, nil
}
// writeGoMod writes go.mod file at dir when Go modules are used.
func writeGoMod(dir, targetPlatform, targetArch string) error {
m, err := areGoModulesUsed()
if err != nil {
return err
}
// If Go modules are not used, go.mod should not be created because the dependencies might not be compatible with Go modules.
if !m {
return nil
}
return writeFile(filepath.Join(dir, "go.mod"), func(w io.Writer) error {
f, err := getModuleVersions(targetPlatform, targetArch, ".")
if err != nil {
return err
}
if f == nil {
return nil
}
bs, err := f.Format()
if err != nil {
return err
}
if _, err := w.Write(bs); err != nil {
return err
}
return nil
})
}
var (
areGoModulesUsedResult struct {
used bool
err error
}
areGoModulesUsedOnce sync.Once
)
func areGoModulesUsed() (bool, error) {
areGoModulesUsedOnce.Do(func() {
out, err := exec.Command("go", "env", "GOMOD").Output()
if err != nil {
areGoModulesUsedResult.err = err
return
}
outstr := strings.TrimSpace(string(out))
areGoModulesUsedResult.used = outstr != ""
})
return areGoModulesUsedResult.used, areGoModulesUsedResult.err
}

View file

@ -0,0 +1,400 @@
// 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.
package main
import (
"archive/zip"
"fmt"
"io"
"os"
"os/exec"
"path/filepath"
"strings"
"golang.org/x/mobile/internal/sdkpath"
"golang.org/x/sync/errgroup"
"golang.org/x/tools/go/packages"
)
func goAndroidBind(gobind string, pkgs []*packages.Package, targets []targetInfo) error {
if _, err := sdkpath.AndroidHome(); err != nil {
return fmt.Errorf("this command requires the Android SDK to be installed: %w", err)
}
// Run gobind to generate the bindings
cmd := exec.Command(
gobind,
"-lang=go,java",
"-outdir="+tmpdir,
)
cmd.Env = append(cmd.Env, "GOOS=android")
cmd.Env = append(cmd.Env, "CGO_ENABLED=1")
if len(buildTags) > 0 {
cmd.Args = append(cmd.Args, "-tags="+strings.Join(buildTags, ","))
}
if bindJavaPkg != "" {
cmd.Args = append(cmd.Args, "-javapkg="+bindJavaPkg)
}
if bindClasspath != "" {
cmd.Args = append(cmd.Args, "-classpath="+bindClasspath)
}
if bindBootClasspath != "" {
cmd.Args = append(cmd.Args, "-bootclasspath="+bindBootClasspath)
}
for _, p := range pkgs {
cmd.Args = append(cmd.Args, p.PkgPath)
}
if err := runCmd(cmd); err != nil {
return err
}
androidDir := filepath.Join(tmpdir, "android")
// Generate binding code and java source code only when processing the first package.
var wg errgroup.Group
for _, t := range targets {
t := t
wg.Go(func() error {
return buildAndroidSO(androidDir, t.arch)
})
}
if err := wg.Wait(); err != nil {
return err
}
jsrc := filepath.Join(tmpdir, "java")
if err := buildAAR(jsrc, androidDir, pkgs, targets); err != nil {
return err
}
return buildSrcJar(jsrc)
}
func buildSrcJar(src string) error {
var out io.Writer = io.Discard
if !buildN {
ext := filepath.Ext(buildO)
f, err := os.Create(buildO[:len(buildO)-len(ext)] + "-sources.jar")
if err != nil {
return err
}
defer func() {
if cerr := f.Close(); err == nil {
err = cerr
}
}()
out = f
}
return writeJar(out, src)
}
// AAR is the format for the binary distribution of an Android Library Project
// and it is a ZIP archive with extension .aar.
// http://tools.android.com/tech-docs/new-build-system/aar-format
//
// These entries are directly at the root of the archive.
//
// AndroidManifest.xml (mandatory)
// classes.jar (mandatory)
// assets/ (optional)
// jni/<abi>/libgojni.so
// R.txt (mandatory)
// res/ (mandatory)
// libs/*.jar (optional, not relevant)
// proguard.txt (optional)
// lint.jar (optional, not relevant)
// aidl (optional, not relevant)
//
// javac and jar commands are needed to build classes.jar.
func buildAAR(srcDir, androidDir string, pkgs []*packages.Package, targets []targetInfo) (err error) {
var out io.Writer = io.Discard
if buildO == "" {
buildO = pkgs[0].Name + ".aar"
}
if !strings.HasSuffix(buildO, ".aar") {
return fmt.Errorf("output file name %q does not end in '.aar'", buildO)
}
if !buildN {
f, err := os.Create(buildO)
if err != nil {
return err
}
defer func() {
if cerr := f.Close(); err == nil {
err = cerr
}
}()
out = f
}
aarw := zip.NewWriter(out)
aarwcreate := func(name string) (io.Writer, error) {
if buildV {
fmt.Fprintf(os.Stderr, "aar: %s\n", name)
}
return aarw.Create(name)
}
w, err := aarwcreate("AndroidManifest.xml")
if err != nil {
return err
}
const manifestFmt = `<manifest xmlns:android="http://schemas.android.com/apk/res/android" package=%q>
<uses-sdk android:minSdkVersion="%d"/></manifest>`
fmt.Fprintf(w, manifestFmt, "go."+pkgs[0].Name+".gojni", buildAndroidAPI)
w, err = aarwcreate("proguard.txt")
if err != nil {
return err
}
fmt.Fprintln(w, `-keep class go.** { *; }`)
if bindJavaPkg != "" {
fmt.Fprintln(w, `-keep class `+bindJavaPkg+`.** { *; }`)
} else {
for _, p := range pkgs {
fmt.Fprintln(w, `-keep class `+p.Name+`.** { *; }`)
}
}
w, err = aarwcreate("classes.jar")
if err != nil {
return err
}
if err := buildJar(w, srcDir); err != nil {
return err
}
files := map[string]string{}
for _, pkg := range pkgs {
// TODO(hajimehoshi): This works only with Go tools that assume all source files are in one directory.
// Fix this to work with other Go tools.
assetsDir := filepath.Join(filepath.Dir(pkg.GoFiles[0]), "assets")
assetsDirExists := false
if fi, err := os.Stat(assetsDir); err == nil {
assetsDirExists = fi.IsDir()
} else if !os.IsNotExist(err) {
return err
}
if assetsDirExists {
err := filepath.Walk(
assetsDir, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if info.IsDir() {
return nil
}
f, err := os.Open(path)
if err != nil {
return err
}
defer f.Close()
name := "assets/" + path[len(assetsDir)+1:]
if orig, exists := files[name]; exists {
return fmt.Errorf("package %s asset name conflict: %s already added from package %s",
pkg.PkgPath, name, orig)
}
files[name] = pkg.PkgPath
w, err := aarwcreate(name)
if err != nil {
return nil
}
_, err = io.Copy(w, f)
return err
})
if err != nil {
return err
}
}
}
for _, t := range targets {
toolchain := ndk.Toolchain(t.arch)
lib := toolchain.abi + "/libgojni.so"
w, err = aarwcreate("jni/" + lib)
if err != nil {
return err
}
if !buildN {
r, err := os.Open(filepath.Join(androidDir, "src/main/jniLibs/"+lib))
if err != nil {
return err
}
defer r.Close()
if _, err := io.Copy(w, r); err != nil {
return err
}
}
}
// TODO(hyangah): do we need to use aapt to create R.txt?
w, err = aarwcreate("R.txt")
if err != nil {
return err
}
w, err = aarwcreate("res/")
if err != nil {
return err
}
return aarw.Close()
}
const (
javacTargetVer = "1.8"
minAndroidAPI = 16
)
func buildJar(w io.Writer, srcDir string) error {
var srcFiles []string
if buildN {
srcFiles = []string{"*.java"}
} else {
err := filepath.Walk(srcDir, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if filepath.Ext(path) == ".java" {
srcFiles = append(srcFiles, filepath.Join(".", path[len(srcDir):]))
}
return nil
})
if err != nil {
return err
}
}
dst := filepath.Join(tmpdir, "javac-output")
if !buildN {
if err := os.MkdirAll(dst, 0700); err != nil {
return err
}
}
bClspath, err := bootClasspath()
if err != nil {
return err
}
args := []string{
"-d", dst,
"-source", javacTargetVer,
"-target", javacTargetVer,
"-bootclasspath", bClspath,
}
if bindClasspath != "" {
args = append(args, "-classpath", bindClasspath)
}
args = append(args, srcFiles...)
javac := exec.Command("javac", args...)
javac.Dir = srcDir
if err := runCmd(javac); err != nil {
return err
}
if buildX {
printcmd("jar c -C %s .", dst)
}
return writeJar(w, dst)
}
func writeJar(w io.Writer, dir string) error {
if buildN {
return nil
}
jarw := zip.NewWriter(w)
jarwcreate := func(name string) (io.Writer, error) {
if buildV {
fmt.Fprintf(os.Stderr, "jar: %s\n", name)
}
return jarw.Create(name)
}
f, err := jarwcreate("META-INF/MANIFEST.MF")
if err != nil {
return err
}
fmt.Fprintf(f, manifestHeader)
err = filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if info.IsDir() {
return nil
}
out, err := jarwcreate(filepath.ToSlash(path[len(dir)+1:]))
if err != nil {
return err
}
in, err := os.Open(path)
if err != nil {
return err
}
defer in.Close()
_, err = io.Copy(out, in)
return err
})
if err != nil {
return err
}
return jarw.Close()
}
// buildAndroidSO generates an Android libgojni.so file to outputDir.
// buildAndroidSO is concurrent-safe.
func buildAndroidSO(outputDir string, arch string) error {
// Copy the environment variables to make this function concurrent-safe.
env := make([]string, len(androidEnv[arch]))
copy(env, androidEnv[arch])
// Add the generated packages to GOPATH for reverse bindings.
gopath := fmt.Sprintf("GOPATH=%s%c%s", tmpdir, filepath.ListSeparator, goEnv("GOPATH"))
env = append(env, gopath)
modulesUsed, err := areGoModulesUsed()
if err != nil {
return err
}
srcDir := filepath.Join(tmpdir, "src")
if modulesUsed {
// Copy the source directory for each architecture for concurrent building.
newSrcDir := filepath.Join(tmpdir, "src-android-"+arch)
if !buildN {
if err := doCopyAll(newSrcDir, srcDir); err != nil {
return err
}
}
srcDir = newSrcDir
if err := writeGoMod(srcDir, "android", arch); err != nil {
return err
}
// Run `go mod tidy` to force to create go.sum.
// Without go.sum, `go build` fails as of Go 1.16.
if err := goModTidyAt(srcDir, env); err != nil {
return err
}
}
toolchain := ndk.Toolchain(arch)
if err := goBuildAt(
srcDir,
"./gobind",
env,
"-buildmode=c-shared",
"-o="+filepath.Join(outputDir, "src", "main", "jniLibs", toolchain.abi, "libgojni.so"),
); err != nil {
return err
}
return nil
}

398
cmd/gomobile/bind_iosapp.go Normal file
View file

@ -0,0 +1,398 @@
// 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.
package main
import (
"bytes"
"encoding/xml"
"errors"
"fmt"
"io"
"os/exec"
"path/filepath"
"strconv"
"strings"
"text/template"
"time"
"golang.org/x/sync/errgroup"
"golang.org/x/tools/go/packages"
)
func goAppleBind(gobind string, pkgs []*packages.Package, targets []targetInfo) error {
var name string
var title string
if buildO == "" {
name = pkgs[0].Name
title = strings.Title(name)
buildO = title + ".xcframework"
} else {
if !strings.HasSuffix(buildO, ".xcframework") {
return fmt.Errorf("static framework name %q missing .xcframework suffix", buildO)
}
base := filepath.Base(buildO)
name = base[:len(base)-len(".xcframework")]
title = strings.Title(name)
}
if err := removeAll(buildO); err != nil {
return err
}
outDirsForPlatform := map[string]string{}
for _, t := range targets {
outDirsForPlatform[t.platform] = filepath.Join(tmpdir, t.platform)
}
// Run the gobind command for each platform
var gobindWG errgroup.Group
for platform, outDir := range outDirsForPlatform {
platform := platform
outDir := outDir
gobindWG.Go(func() error {
// Catalyst support requires iOS 13+
v, _ := strconv.ParseFloat(buildIOSVersion, 64)
if platform == "maccatalyst" && v < 13.0 {
return errors.New("catalyst requires -iosversion=13 or higher")
}
// Run gobind once per platform to generate the bindings
cmd := exec.Command(
gobind,
"-lang=go,objc",
"-outdir="+outDir,
)
cmd.Env = append(cmd.Env, "GOOS="+platformOS(platform))
cmd.Env = append(cmd.Env, "CGO_ENABLED=1")
tags := append(buildTags[:], platformTags(platform)...)
cmd.Args = append(cmd.Args, "-tags="+strings.Join(tags, ","))
if bindPrefix != "" {
cmd.Args = append(cmd.Args, "-prefix="+bindPrefix)
}
for _, p := range pkgs {
cmd.Args = append(cmd.Args, p.PkgPath)
}
if err := runCmd(cmd); err != nil {
return err
}
return nil
})
}
if err := gobindWG.Wait(); err != nil {
return err
}
modulesUsed, err := areGoModulesUsed()
if err != nil {
return err
}
// Build archive files.
var buildWG errgroup.Group
for _, t := range targets {
t := t
buildWG.Go(func() error {
outDir := outDirsForPlatform[t.platform]
outSrcDir := filepath.Join(outDir, "src")
if modulesUsed {
// Copy the source directory for each architecture for concurrent building.
newOutSrcDir := filepath.Join(outDir, "src-"+t.arch)
if !buildN {
if err := doCopyAll(newOutSrcDir, outSrcDir); err != nil {
return err
}
}
outSrcDir = newOutSrcDir
}
// Copy the environment variables to make this function concurrent-safe.
env := make([]string, len(appleEnv[t.String()]))
copy(env, appleEnv[t.String()])
// Add the generated packages to GOPATH for reverse bindings.
gopath := fmt.Sprintf("GOPATH=%s%c%s", outDir, filepath.ListSeparator, goEnv("GOPATH"))
env = append(env, gopath)
// Run `go mod tidy` to force to create go.sum.
// Without go.sum, `go build` fails as of Go 1.16.
if modulesUsed {
if err := writeGoMod(outSrcDir, t.platform, t.arch); err != nil {
return err
}
if err := goModTidyAt(outSrcDir, env); err != nil {
return err
}
}
if err := goAppleBindArchive(appleArchiveFilepath(name, t), env, outSrcDir); err != nil {
return fmt.Errorf("%s/%s: %v", t.platform, t.arch, err)
}
return nil
})
}
if err := buildWG.Wait(); err != nil {
return err
}
var frameworkDirs []string
frameworkArchCount := map[string]int{}
for _, t := range targets {
outDir := outDirsForPlatform[t.platform]
gobindDir := filepath.Join(outDir, "src", "gobind")
env := appleEnv[t.String()][:]
sdk := getenv(env, "DARWIN_SDK")
frameworkDir := filepath.Join(tmpdir, t.platform, sdk, title+".framework")
frameworkDirs = append(frameworkDirs, frameworkDir)
frameworkArchCount[frameworkDir] = frameworkArchCount[frameworkDir] + 1
frameworkLayout, err := frameworkLayoutForTarget(t, title)
if err != nil {
return err
}
titlePath := filepath.Join(frameworkDir, frameworkLayout.binaryPath, title)
if frameworkArchCount[frameworkDir] > 1 {
// Not the first static lib, attach to a fat library and skip create headers
fatCmd := exec.Command(
"xcrun",
"lipo", appleArchiveFilepath(name, t), titlePath, "-create", "-output", titlePath,
)
if err := runCmd(fatCmd); err != nil {
return err
}
continue
}
headersDir := filepath.Join(frameworkDir, frameworkLayout.headerPath)
if err := mkdir(headersDir); err != nil {
return err
}
lipoCmd := exec.Command(
"xcrun",
"lipo", appleArchiveFilepath(name, t), "-create", "-o", titlePath,
)
if err := runCmd(lipoCmd); err != nil {
return err
}
fileBases := make([]string, len(pkgs)+1)
for i, pkg := range pkgs {
fileBases[i] = bindPrefix + strings.Title(pkg.Name)
}
fileBases[len(fileBases)-1] = "Universe"
// Copy header file next to output archive.
var headerFiles []string
if len(fileBases) == 1 {
headerFiles = append(headerFiles, title+".h")
err := copyFile(
filepath.Join(headersDir, title+".h"),
filepath.Join(gobindDir, bindPrefix+title+".objc.h"),
)
if err != nil {
return err
}
} else {
for _, fileBase := range fileBases {
headerFiles = append(headerFiles, fileBase+".objc.h")
err := copyFile(
filepath.Join(headersDir, fileBase+".objc.h"),
filepath.Join(gobindDir, fileBase+".objc.h"),
)
if err != nil {
return err
}
}
err := copyFile(
filepath.Join(headersDir, "ref.h"),
filepath.Join(gobindDir, "ref.h"),
)
if err != nil {
return err
}
headerFiles = append(headerFiles, title+".h")
err = writeFile(filepath.Join(headersDir, title+".h"), func(w io.Writer) error {
return appleBindHeaderTmpl.Execute(w, map[string]interface{}{
"pkgs": pkgs, "title": title, "bases": fileBases,
})
})
if err != nil {
return err
}
}
frameworkInfoPlistDir := filepath.Join(frameworkDir, frameworkLayout.infoPlistPath)
if err := mkdir(frameworkInfoPlistDir); err != nil {
return err
}
err = writeFile(filepath.Join(frameworkInfoPlistDir, "Info.plist"), func(w io.Writer) error {
fmVersion := fmt.Sprintf("0.0.%d", time.Now().Unix())
infoFrameworkPlistlData := infoFrameworkPlistlData{
BundleID: escapePlistValue(rfc1034Label(title)),
ExecutableName: escapePlistValue(title),
Version: escapePlistValue(fmVersion),
}
infoplist := new(bytes.Buffer)
if err := infoFrameworkPlistTmpl.Execute(infoplist, infoFrameworkPlistlData); err != nil {
return err
}
_, err := w.Write(infoplist.Bytes())
return err
})
if err != nil {
return err
}
var mmVals = struct {
Module string
Headers []string
}{
Module: title,
Headers: headerFiles,
}
modulesDir := filepath.Join(frameworkDir, frameworkLayout.modulePath)
err = writeFile(filepath.Join(modulesDir, "module.modulemap"), func(w io.Writer) error {
return appleModuleMapTmpl.Execute(w, mmVals)
})
if err != nil {
return err
}
for src, dst := range frameworkLayout.symlinks {
if err := symlink(src, filepath.Join(frameworkDir, dst)); err != nil {
return err
}
}
}
// Finally combine all frameworks to an XCFramework
xcframeworkArgs := []string{"-create-xcframework"}
for _, dir := range frameworkDirs {
// On macOS, a temporary directory starts with /var, which is a symbolic link to /private/var.
// And in gomobile, a temporary directory is usually used as a working directly.
// Unfortunately, xcodebuild in Xcode 15 seems to have a bug and might not be able to understand fullpaths with symbolic links.
// As a workaround, resolve the path with symbolic links by filepath.EvalSymlinks.
dir, err := filepath.EvalSymlinks(dir)
if err != nil {
return err
}
xcframeworkArgs = append(xcframeworkArgs, "-framework", dir)
}
xcframeworkArgs = append(xcframeworkArgs, "-output", buildO)
cmd := exec.Command("xcodebuild", xcframeworkArgs...)
err = runCmd(cmd)
return err
}
type frameworkLayout struct {
headerPath string
binaryPath string
modulePath string
infoPlistPath string
// symlinks to create in the framework. Maps src (relative to dst) -> dst (relative to framework bundle root)
symlinks map[string]string
}
// frameworkLayoutForTarget generates the filestructure for a framework for the given target platform (macos, ios, etc),
// according to Apple's spec https://developer.apple.com/documentation/bundleresources/placing_content_in_a_bundle
func frameworkLayoutForTarget(t targetInfo, title string) (*frameworkLayout, error) {
switch t.platform {
case "macos", "maccatalyst":
return &frameworkLayout{
headerPath: "Versions/A/Headers",
binaryPath: "Versions/A",
modulePath: "Versions/A/Modules",
infoPlistPath: "Versions/A/Resources",
symlinks: map[string]string{
"A": "Versions/Current",
"Versions/Current/Resources": "Resources",
"Versions/Current/Headers": "Headers",
"Versions/Current/Modules": "Modules",
filepath.Join("Versions/Current", title): title,
},
}, nil
case "ios", "iossimulator":
return &frameworkLayout{
headerPath: "Headers",
binaryPath: ".",
modulePath: "Modules",
infoPlistPath: ".",
}, nil
}
return nil, fmt.Errorf("unsupported platform %q", t.platform)
}
type infoFrameworkPlistlData struct {
BundleID string
ExecutableName string
Version string
}
// infoFrameworkPlistTmpl is a template for the Info.plist file in a framework.
// Minimum OS version == 100.0 is a workaround for SPM issue
// https://github.com/firebase/firebase-ios-sdk/pull/12439/files#diff-f4eb4ff5ec89af999cbe8fa3ffe5647d7853ffbc9c1515b337ca043c684b6bb4R679
var infoFrameworkPlistTmpl = template.Must(template.New("infoFrameworkPlist").Parse(`<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleExecutable</key>
<string>{{.ExecutableName}}</string>
<key>CFBundleIdentifier</key>
<string>{{.BundleID}}</string>
<key>MinimumOSVersion</key>
<string>100.0</string>
<key>CFBundleShortVersionString</key>
<string>{{.Version}}</string>
<key>CFBundleVersion</key>
<string>{{.Version}}</string>
<key>CFBundlePackageType</key>
<string>FMWK</string>
</dict>
</plist>
`))
func escapePlistValue(value string) string {
var b bytes.Buffer
xml.EscapeText(&b, []byte(value))
return b.String()
}
var appleModuleMapTmpl = template.Must(template.New("iosmmap").Parse(`framework module "{{.Module}}" {
header "ref.h"
{{range .Headers}} header "{{.}}"
{{end}}
export *
}`))
func appleArchiveFilepath(name string, t targetInfo) string {
return filepath.Join(tmpdir, name+"-"+t.platform+"-"+t.arch+".a")
}
func goAppleBindArchive(out string, env []string, gosrc string) error {
return goBuildAt(gosrc, "./gobind", env, "-buildmode=c-archive", "-o", out)
}
var appleBindHeaderTmpl = template.Must(template.New("apple.h").Parse(`
// Objective-C API for talking to the following Go packages
//
{{range .pkgs}}// {{.PkgPath}}
{{end}}//
// File is generated by gomobile bind. Do not edit.
#ifndef __{{.title}}_FRAMEWORK_H__
#define __{{.title}}_FRAMEWORK_H__
{{range .bases}}#include "{{.}}.objc.h"
{{end}}
#endif
`))

404
cmd/gomobile/bind_test.go Normal file
View file

@ -0,0 +1,404 @@
// 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.
package main
import (
"bytes"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"testing"
"text/template"
"golang.org/x/mobile/internal/sdkpath"
)
func TestBindAndroid(t *testing.T) {
platform, err := sdkpath.AndroidAPIPath(minAndroidAPI)
if err != nil {
t.Skip("No compatible Android API platform found, skipping bind")
}
// platform is a path like "/path/to/Android/sdk/platforms/android-32"
components := strings.Split(platform, string(filepath.Separator))
if len(components) < 2 {
t.Fatalf("API path is too short: %s", platform)
}
components = components[len(components)-2:]
platformRel := filepath.Join("$ANDROID_HOME", components[0], components[1])
defer func() {
xout = os.Stderr
buildN = false
buildX = false
buildO = ""
buildTarget = ""
bindJavaPkg = ""
}()
buildN = true
buildX = true
buildO = "asset.aar"
buildTarget = "android/arm"
tests := []struct {
javaPkg string
}{
{
// Empty javaPkg
},
{
javaPkg: "com.example.foo",
},
}
for _, tc := range tests {
bindJavaPkg = tc.javaPkg
buf := new(bytes.Buffer)
xout = buf
gopath = filepath.SplitList(goEnv("GOPATH"))[0]
if goos == "windows" {
os.Setenv("HOMEDRIVE", "C:")
}
cmdBind.flag.Parse([]string{"golang.org/x/mobile/asset"})
err := runBind(cmdBind)
if err != nil {
t.Log(buf.String())
t.Fatal(err)
}
got := filepath.ToSlash(buf.String())
output, err := defaultOutputData("")
if err != nil {
t.Fatal(err)
}
data := struct {
outputData
AndroidPlatform string
JavaPkg string
}{
outputData: output,
AndroidPlatform: platformRel,
JavaPkg: tc.javaPkg,
}
wantBuf := new(bytes.Buffer)
if err := bindAndroidTmpl.Execute(wantBuf, data); err != nil {
t.Errorf("%+v: computing diff failed: %v", tc, err)
continue
}
diff, err := diff(got, wantBuf.String())
if err != nil {
t.Errorf("%+v: computing diff failed: %v", tc, err)
continue
}
if diff != "" {
t.Errorf("%+v: unexpected output:\n%s", tc, diff)
}
}
}
func TestBindApple(t *testing.T) {
if !xcodeAvailable() {
t.Skip("Xcode is missing")
}
defer func() {
xout = os.Stderr
buildN = false
buildX = false
buildO = ""
buildTarget = ""
bindPrefix = ""
}()
buildN = true
buildX = true
buildO = "Asset.xcframework"
buildTarget = "ios/arm64"
tests := []struct {
prefix string
out string
}{
{
// empty prefix
},
{
prefix: "Foo",
},
{
out: "Abcde.xcframework",
},
}
for _, tc := range tests {
bindPrefix = tc.prefix
if tc.out != "" {
buildO = tc.out
}
buf := new(bytes.Buffer)
xout = buf
gopath = filepath.SplitList(goEnv("GOPATH"))[0]
if goos == "windows" {
os.Setenv("HOMEDRIVE", "C:")
}
cmdBind.flag.Parse([]string{"golang.org/x/mobile/asset"})
if err := runBind(cmdBind); err != nil {
t.Log(buf.String())
t.Fatal(err)
}
got := filepath.ToSlash(buf.String())
output, err := defaultOutputData("")
if err != nil {
t.Fatal(err)
}
data := struct {
outputData
Output string
Prefix string
}{
outputData: output,
Output: buildO[:len(buildO)-len(".xcframework")],
Prefix: tc.prefix,
}
wantBuf := new(bytes.Buffer)
if err := bindAppleTmpl.Execute(wantBuf, data); err != nil {
t.Errorf("%+v: computing diff failed: %v", tc, err)
continue
}
diff, err := diff(got, wantBuf.String())
if err != nil {
t.Errorf("%+v: computing diff failed: %v", tc, err)
continue
}
if diff != "" {
t.Errorf("%+v: unexpected output:\n%s", tc, diff)
}
}
}
var bindAndroidTmpl = template.Must(template.New("output").Parse(`GOMOBILE={{.GOPATH}}/pkg/gomobile
WORK=$WORK
GOOS=android CGO_ENABLED=1 gobind -lang=go,java -outdir=$WORK{{if .JavaPkg}} -javapkg={{.JavaPkg}}{{end}} golang.org/x/mobile/asset
mkdir -p $WORK/src-android-arm
PWD=$WORK/src-android-arm GOMODCACHE=$GOPATH/pkg/mod GOOS=android GOARCH=arm CC=$NDK_PATH/toolchains/llvm/prebuilt/{{.NDKARCH}}/bin/armv7a-linux-androideabi16-clang CXX=$NDK_PATH/toolchains/llvm/prebuilt/{{.NDKARCH}}/bin/armv7a-linux-androideabi16-clang++ CGO_ENABLED=1 GOARM=7 GOPATH=$WORK:$GOPATH go mod tidy
PWD=$WORK/src-android-arm GOMODCACHE=$GOPATH/pkg/mod GOOS=android GOARCH=arm CC=$NDK_PATH/toolchains/llvm/prebuilt/{{.NDKARCH}}/bin/armv7a-linux-androideabi16-clang CXX=$NDK_PATH/toolchains/llvm/prebuilt/{{.NDKARCH}}/bin/armv7a-linux-androideabi16-clang++ CGO_ENABLED=1 GOARM=7 GOPATH=$WORK:$GOPATH go build -x -buildmode=c-shared -o=$WORK/android/src/main/jniLibs/armeabi-v7a/libgojni.so ./gobind
PWD=$WORK/java javac -d $WORK/javac-output -source 1.8 -target 1.8 -bootclasspath {{.AndroidPlatform}}/android.jar *.java
jar c -C $WORK/javac-output .
`))
var bindAppleTmpl = template.Must(template.New("output").Parse(`GOMOBILE={{.GOPATH}}/pkg/gomobile
WORK=$WORK
rm -r -f "{{.Output}}.xcframework"
GOOS=ios CGO_ENABLED=1 gobind -lang=go,objc -outdir=$WORK/ios -tags=ios{{if .Prefix}} -prefix={{.Prefix}}{{end}} golang.org/x/mobile/asset
mkdir -p $WORK/ios/src-arm64
PWD=$WORK/ios/src-arm64 GOMODCACHE=$GOPATH/pkg/mod GOOS=ios GOARCH=arm64 GOFLAGS=-tags=ios CC=iphoneos-clang CXX=iphoneos-clang++ CGO_CFLAGS=-isysroot iphoneos -miphoneos-version-min=13.0 -fembed-bitcode -arch arm64 CGO_CXXFLAGS=-isysroot iphoneos -miphoneos-version-min=13.0 -fembed-bitcode -arch arm64 CGO_LDFLAGS=-isysroot iphoneos -miphoneos-version-min=13.0 -fembed-bitcode -arch arm64 CGO_ENABLED=1 DARWIN_SDK=iphoneos GOPATH=$WORK/ios:$GOPATH go mod tidy
PWD=$WORK/ios/src-arm64 GOMODCACHE=$GOPATH/pkg/mod GOOS=ios GOARCH=arm64 GOFLAGS=-tags=ios CC=iphoneos-clang CXX=iphoneos-clang++ CGO_CFLAGS=-isysroot iphoneos -miphoneos-version-min=13.0 -fembed-bitcode -arch arm64 CGO_CXXFLAGS=-isysroot iphoneos -miphoneos-version-min=13.0 -fembed-bitcode -arch arm64 CGO_LDFLAGS=-isysroot iphoneos -miphoneos-version-min=13.0 -fembed-bitcode -arch arm64 CGO_ENABLED=1 DARWIN_SDK=iphoneos GOPATH=$WORK/ios:$GOPATH go build -x -buildmode=c-archive -o $WORK/{{.Output}}-ios-arm64.a ./gobind
mkdir -p $WORK/ios/iphoneos/{{.Output}}.framework/Versions/A/Headers
ln -s A $WORK/ios/iphoneos/{{.Output}}.framework/Versions/Current
ln -s Versions/Current/Headers $WORK/ios/iphoneos/{{.Output}}.framework/Headers
ln -s Versions/Current/{{.Output}} $WORK/ios/iphoneos/{{.Output}}.framework/{{.Output}}
xcrun lipo $WORK/{{.Output}}-ios-arm64.a -create -o $WORK/ios/iphoneos/{{.Output}}.framework/Versions/A/{{.Output}}
cp $WORK/ios/src/gobind/{{.Prefix}}Asset.objc.h $WORK/ios/iphoneos/{{.Output}}.framework/Versions/A/Headers/{{.Prefix}}Asset.objc.h
mkdir -p $WORK/ios/iphoneos/{{.Output}}.framework/Versions/A/Headers
cp $WORK/ios/src/gobind/Universe.objc.h $WORK/ios/iphoneos/{{.Output}}.framework/Versions/A/Headers/Universe.objc.h
mkdir -p $WORK/ios/iphoneos/{{.Output}}.framework/Versions/A/Headers
cp $WORK/ios/src/gobind/ref.h $WORK/ios/iphoneos/{{.Output}}.framework/Versions/A/Headers/ref.h
mkdir -p $WORK/ios/iphoneos/{{.Output}}.framework/Versions/A/Headers
mkdir -p $WORK/ios/iphoneos/{{.Output}}.framework/Versions/A/Headers
mkdir -p $WORK/ios/iphoneos/{{.Output}}.framework/Versions/A/Resources
ln -s Versions/Current/Resources $WORK/ios/iphoneos/{{.Output}}.framework/Resources
mkdir -p $WORK/ios/iphoneos/{{.Output}}.framework/Resources
mkdir -p $WORK/ios/iphoneos/{{.Output}}.framework/Versions/A/Modules
ln -s Versions/Current/Modules $WORK/ios/iphoneos/{{.Output}}.framework/Modules
xcodebuild -create-xcframework -framework $WORK/ios/iphoneos/{{.Output}}.framework -output {{.Output}}.xcframework
`))
func TestBindAppleAll(t *testing.T) {
if !xcodeAvailable() {
t.Skip("Xcode is missing")
}
defer func() {
xout = os.Stderr
buildN = false
buildX = false
buildO = ""
buildTarget = ""
bindPrefix = ""
}()
buildN = true
buildX = true
buildO = "Asset.xcframework"
buildTarget = "ios"
buf := new(bytes.Buffer)
xout = buf
gopath = filepath.SplitList(goEnv("GOPATH"))[0]
if goos == "windows" {
os.Setenv("HOMEDRIVE", "C:")
}
cmdBind.flag.Parse([]string{"golang.org/x/mobile/asset"})
if err := runBind(cmdBind); err != nil {
t.Log(buf.String())
t.Fatal(err)
}
}
const ambiguousPathsGoMod = `module ambiguouspaths
go 1.18
require golang.org/x/mobile v0.0.0-20230905140555-fbe1c053b6a9
require (
golang.org/x/exp/shiny v0.0.0-20230817173708-d852ddb80c63 // indirect
golang.org/x/image v0.11.0 // indirect
golang.org/x/sys v0.11.0 // indirect
)
`
const ambiguousPathsGoSum = `github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/exp/shiny v0.0.0-20230817173708-d852ddb80c63 h1:3AGKexOYqL+ztdWdkB1bDwXgPBuTS/S8A4WzuTvJ8Cg=
golang.org/x/exp/shiny v0.0.0-20230817173708-d852ddb80c63/go.mod h1:UH99kUObWAZkDnWqppdQe5ZhPYESUw8I0zVV1uWBR+0=
golang.org/x/image v0.11.0 h1:ds2RoQvBvYTiJkwpSFDwCcDFNX7DqjL2WsUgTNk0Ooo=
golang.org/x/image v0.11.0/go.mod h1:bglhjqbqVuEb9e9+eNR45Jfu7D+T4Qan+NhQk8Ck2P8=
golang.org/x/mobile v0.0.0-20230905140555-fbe1c053b6a9 h1:LaLfQUz4L1tfuOlrtEouZLZ0qHDwKn87E1NKoiudP/o=
golang.org/x/mobile v0.0.0-20230905140555-fbe1c053b6a9/go.mod h1:2jxcxt/JNJik+N+QcB8q308+SyrE3bu43+sGZDmJ02M=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM=
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
`
const ambiguousPathsGo = `package ambiguouspaths
import (
_ "golang.org/x/mobile/app"
)
func Dummy() {}
`
func TestBindWithGoModules(t *testing.T) {
if runtime.GOOS == "android" || runtime.GOOS == "ios" {
t.Skipf("gomobile and gobind are not available on %s", runtime.GOOS)
}
dir := t.TempDir()
if out, err := exec.Command("go", "build", "-o="+dir, "golang.org/x/mobile/cmd/gobind").CombinedOutput(); err != nil {
t.Fatalf("%v: %s", err, string(out))
}
if out, err := exec.Command("go", "build", "-o="+dir, "golang.org/x/mobile/cmd/gomobile").CombinedOutput(); err != nil {
t.Fatalf("%v: %s", err, string(out))
}
path := dir
if p := os.Getenv("PATH"); p != "" {
path += string(filepath.ListSeparator) + p
}
// Create a source package dynamically to avoid go.mod files in this repository. See golang/go#34352 for more details.
if err := os.Mkdir(filepath.Join(dir, "ambiguouspaths"), 0755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(dir, "ambiguouspaths", "go.mod"), []byte(ambiguousPathsGoMod), 0644); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(dir, "ambiguouspaths", "go.sum"), []byte(ambiguousPathsGoSum), 0644); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(dir, "ambiguouspaths", "ambiguouspaths.go"), []byte(ambiguousPathsGo), 0644); err != nil {
t.Fatal(err)
}
for _, target := range []string{"android", "ios"} {
target := target
t.Run(target, func(t *testing.T) {
switch target {
case "android":
if _, err := sdkpath.AndroidAPIPath(minAndroidAPI); err != nil {
t.Skip("No compatible Android API platform found, skipping bind")
}
case "ios":
if !xcodeAvailable() {
t.Skip("Xcode is missing")
}
}
var out string
switch target {
case "android":
out = filepath.Join(dir, "cgopkg.aar")
case "ios":
out = filepath.Join(dir, "Cgopkg.xcframework")
}
tests := []struct {
Name string
Path string
Dir string
}{
{
Name: "Absolute Path",
Path: "golang.org/x/mobile/bind/testdata/cgopkg",
},
{
Name: "Relative Path",
Path: "./bind/testdata/cgopkg",
Dir: filepath.Join("..", ".."),
},
{
Name: "Ambiguous Paths",
Path: ".",
Dir: filepath.Join(dir, "ambiguouspaths"),
},
}
for _, tc := range tests {
tc := tc
t.Run(tc.Name, func(t *testing.T) {
cmd := exec.Command(filepath.Join(dir, "gomobile"), "bind", "-target="+target, "-o="+out, tc.Path)
cmd.Env = append(os.Environ(), "PATH="+path, "GO111MODULE=on")
cmd.Dir = tc.Dir
if out, err := cmd.CombinedOutput(); err != nil {
t.Errorf("gomobile bind failed: %v\n%s", err, string(out))
}
})
}
})
}
}

448
cmd/gomobile/build.go Normal file
View file

@ -0,0 +1,448 @@
// 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 main
import (
"bufio"
"errors"
"fmt"
"io"
"os"
"os/exec"
"regexp"
"strconv"
"strings"
"golang.org/x/mobile/internal/sdkpath"
"golang.org/x/tools/go/packages"
)
var tmpdir string
var cmdBuild = &command{
run: runBuild,
Name: "build",
Usage: "[-target android|" + strings.Join(applePlatforms, "|") + "] [-o output] [-bundleid bundleID] [build flags] [package]",
Short: "compile android APK and iOS app",
Long: `
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 (` + strings.Join(applePlatforms, ", ") + `).
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.
Flag -iosversion sets the minimal version of the iOS SDK to compile against.
The default version is 13.0.
Flag -androidapi sets the Android API version to compile against.
The default and minimum is 16.
The -bundleid flag is required for -target ios and sets the bundle ID to use
with the app.
The -o flag specifies the output file name. If not specified, the
output file name depends on the package built.
The -v flag provides verbose output, including the list of packages built.
The build flags -a, -i, -n, -x, -gcflags, -ldflags, -tags, -trimpath, and -work are
shared with the build command. For documentation, see 'go help build'.
`,
}
func runBuild(cmd *command) (err error) {
_, err = runBuildImpl(cmd)
return
}
// runBuildImpl builds a package for mobiles based on the given commands.
// runBuildImpl returns a built package information and an error if exists.
func runBuildImpl(cmd *command) (*packages.Package, error) {
cleanup, err := buildEnvInit()
if err != nil {
return nil, err
}
defer cleanup()
args := cmd.flag.Args()
targets, err := parseBuildTarget(buildTarget)
if err != nil {
return nil, fmt.Errorf(`invalid -target=%q: %v`, buildTarget, err)
}
var buildPath string
switch len(args) {
case 0:
buildPath = "."
case 1:
buildPath = args[0]
default:
cmd.usage()
os.Exit(1)
}
// 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(targets[0]), buildPath)
if err != nil {
return nil, err
}
// len(pkgs) can be more than 1 e.g., when the specified path includes `...`.
if len(pkgs) != 1 {
cmd.usage()
os.Exit(1)
}
pkg := pkgs[0]
if pkg.Name != "main" && buildO != "" {
return nil, fmt.Errorf("cannot set -o when building non-main package")
}
var nmpkgs map[string]bool
switch {
case isAndroidPlatform(targets[0].platform):
if pkg.Name != "main" {
for _, t := range targets {
if err := goBuild(pkg.PkgPath, androidEnv[t.arch]); err != nil {
return nil, err
}
}
return pkg, nil
}
nmpkgs, err = goAndroidBuild(pkg, targets)
if err != nil {
return nil, err
}
case isApplePlatform(targets[0].platform):
if !xcodeAvailable() {
return nil, fmt.Errorf("-target=%s requires XCode", buildTarget)
}
if pkg.Name != "main" {
for _, t := range targets {
// Catalyst support requires iOS 13+
v, _ := strconv.ParseFloat(buildIOSVersion, 64)
if t.platform == "maccatalyst" && v < 13.0 {
return nil, errors.New("catalyst requires -iosversion=13 or higher")
}
if err := goBuild(pkg.PkgPath, appleEnv[t.String()]); err != nil {
return nil, err
}
}
return pkg, nil
}
if buildBundleID == "" {
return nil, fmt.Errorf("-target=%s requires -bundleid set", buildTarget)
}
nmpkgs, err = goAppleBuild(pkg, buildBundleID, targets)
if err != nil {
return nil, err
}
}
if !nmpkgs["golang.org/x/mobile/app"] {
return nil, fmt.Errorf(`%s does not import "golang.org/x/mobile/app"`, pkg.PkgPath)
}
return pkg, nil
}
var nmRE = regexp.MustCompile(`[0-9a-f]{8} t _?(?:.*/vendor/)?(golang.org/x.*/[^.]*)`)
func extractPkgs(nm string, path string) (map[string]bool, error) {
if buildN {
return map[string]bool{"golang.org/x/mobile/app": true}, nil
}
r, w := io.Pipe()
cmd := exec.Command(nm, path)
cmd.Stdout = w
cmd.Stderr = os.Stderr
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 := cmd.Run()
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
}
var xout io.Writer = os.Stderr
func printcmd(format string, args ...interface{}) {
cmd := fmt.Sprintf(format+"\n", args...)
if tmpdir != "" {
cmd = strings.Replace(cmd, tmpdir, "$WORK", -1)
}
if androidHome, err := sdkpath.AndroidHome(); err == nil {
cmd = strings.Replace(cmd, androidHome, "$ANDROID_HOME", -1)
}
if gomobilepath != "" {
cmd = strings.Replace(cmd, gomobilepath, "$GOMOBILE", -1)
}
if gopath := goEnv("GOPATH"); gopath != "" {
cmd = strings.Replace(cmd, gopath, "$GOPATH", -1)
}
if env := os.Getenv("HOMEPATH"); env != "" {
cmd = strings.Replace(cmd, env, "$HOMEPATH", -1)
}
fmt.Fprint(xout, cmd)
}
// "Build flags", used by multiple commands.
var (
buildA bool // -a
buildI bool // -i
buildN bool // -n
buildV bool // -v
buildX bool // -x
buildO string // -o
buildGcflags string // -gcflags
buildLdflags string // -ldflags
buildTarget string // -target
buildTrimpath bool // -trimpath
buildWork bool // -work
buildBundleID string // -bundleid
buildIOSVersion string // -iosversion
buildAndroidAPI int // -androidapi
buildTags stringsFlag // -tags
)
func addBuildFlags(cmd *command) {
cmd.flag.StringVar(&buildO, "o", "", "")
cmd.flag.StringVar(&buildGcflags, "gcflags", "", "")
cmd.flag.StringVar(&buildLdflags, "ldflags", "", "")
cmd.flag.StringVar(&buildTarget, "target", "android", "")
cmd.flag.StringVar(&buildBundleID, "bundleid", "", "")
cmd.flag.StringVar(&buildIOSVersion, "iosversion", "13.0", "")
cmd.flag.IntVar(&buildAndroidAPI, "androidapi", minAndroidAPI, "")
cmd.flag.BoolVar(&buildA, "a", false, "")
cmd.flag.BoolVar(&buildI, "i", false, "")
cmd.flag.BoolVar(&buildTrimpath, "trimpath", false, "")
cmd.flag.Var(&buildTags, "tags", "")
}
func addBuildFlagsNVXWork(cmd *command) {
cmd.flag.BoolVar(&buildN, "n", false, "")
cmd.flag.BoolVar(&buildV, "v", false, "")
cmd.flag.BoolVar(&buildX, "x", false, "")
cmd.flag.BoolVar(&buildWork, "work", false, "")
}
func init() {
addBuildFlags(cmdBuild)
addBuildFlagsNVXWork(cmdBuild)
addBuildFlags(cmdInstall)
addBuildFlagsNVXWork(cmdInstall)
addBuildFlagsNVXWork(cmdInit)
addBuildFlags(cmdBind)
addBuildFlagsNVXWork(cmdBind)
addBuildFlagsNVXWork(cmdClean)
}
func goBuild(src string, env []string, args ...string) error {
return goCmd("build", []string{src}, env, args...)
}
func goBuildAt(at string, src string, env []string, args ...string) error {
return goCmdAt(at, "build", []string{src}, env, args...)
}
func goInstall(srcs []string, env []string, args ...string) error {
return goCmd("install", srcs, env, args...)
}
func goCmd(subcmd string, srcs []string, env []string, args ...string) error {
return goCmdAt("", subcmd, srcs, env, args...)
}
func goCmdAt(at string, subcmd string, srcs []string, env []string, args ...string) error {
cmd := exec.Command("go", subcmd)
tags := buildTags
if len(tags) > 0 {
cmd.Args = append(cmd.Args, "-tags", strings.Join(tags, ","))
}
if buildV {
cmd.Args = append(cmd.Args, "-v")
}
if subcmd != "install" && buildI {
cmd.Args = append(cmd.Args, "-i")
}
if buildX {
cmd.Args = append(cmd.Args, "-x")
}
if buildGcflags != "" {
cmd.Args = append(cmd.Args, "-gcflags", buildGcflags)
}
if buildLdflags != "" {
cmd.Args = append(cmd.Args, "-ldflags", buildLdflags)
}
if buildTrimpath {
cmd.Args = append(cmd.Args, "-trimpath")
}
if buildWork {
cmd.Args = append(cmd.Args, "-work")
}
cmd.Args = append(cmd.Args, args...)
cmd.Args = append(cmd.Args, srcs...)
// 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 {
env = append([]string{"GOMODCACHE=" + gmc}, env...)
} else {
env = append([]string{}, env...)
}
cmd.Env = env
cmd.Dir = at
return runCmd(cmd)
}
func goModTidyAt(at string, env []string) error {
cmd := exec.Command("go", "mod", "tidy")
if buildV {
cmd.Args = append(cmd.Args, "-v")
}
// 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 {
env = append([]string{"GOMODCACHE=" + gmc}, env...)
} else {
env = append([]string{}, env...)
}
cmd.Env = env
cmd.Dir = at
return runCmd(cmd)
}
// parseBuildTarget parses buildTarget into 1 or more platforms and architectures.
// Returns an error if buildTarget contains invalid input.
// Example valid target strings:
//
// android
// android/arm64,android/386,android/amd64
// ios,iossimulator,maccatalyst
// macos/amd64
func parseBuildTarget(buildTarget string) ([]targetInfo, error) {
if buildTarget == "" {
return nil, fmt.Errorf(`invalid target ""`)
}
targets := []targetInfo{}
targetsAdded := make(map[targetInfo]bool)
addTarget := func(platform, arch string) {
t := targetInfo{platform, arch}
if targetsAdded[t] {
return
}
targets = append(targets, t)
targetsAdded[t] = true
}
addPlatform := func(platform string) {
for _, arch := range platformArchs(platform) {
addTarget(platform, arch)
}
}
var isAndroid, isApple bool
for _, target := range strings.Split(buildTarget, ",") {
tuple := strings.SplitN(target, "/", 2)
platform := tuple[0]
hasArch := len(tuple) == 2
if isAndroidPlatform(platform) {
isAndroid = true
} else if isApplePlatform(platform) {
isApple = true
} else {
return nil, fmt.Errorf("unsupported platform: %q", platform)
}
if isAndroid && isApple {
return nil, fmt.Errorf(`cannot mix android and Apple platforms`)
}
if hasArch {
arch := tuple[1]
if !isSupportedArch(platform, arch) {
return nil, fmt.Errorf(`unsupported platform/arch: %q`, target)
}
addTarget(platform, arch)
} else {
addPlatform(platform)
}
}
// Special case to build iossimulator if -target=ios
if buildTarget == "ios" {
addPlatform("iossimulator")
}
return targets, nil
}
type targetInfo struct {
platform string
arch string
}
func (t targetInfo) String() string {
return t.platform + "/" + t.arch
}
func goModCachePath() (string, error) {
out, err := exec.Command("go", "env", "GOMODCACHE").Output()
if err != nil {
return "", err
}
return strings.TrimSpace(string(out)), nil
}

View file

@ -0,0 +1,357 @@
// 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.
package main
import (
"bytes"
"crypto/x509"
"encoding/base64"
"encoding/pem"
"encoding/xml"
"errors"
"fmt"
"io"
"log"
"os"
"path"
"path/filepath"
"strings"
"golang.org/x/mobile/internal/binres"
"golang.org/x/tools/go/packages"
)
func goAndroidBuild(pkg *packages.Package, targets []targetInfo) (map[string]bool, error) {
ndkRoot, err := ndkRoot(targets...)
if err != nil {
return nil, err
}
appName := path.Base(pkg.PkgPath)
libName := androidPkgName(appName)
// TODO(hajimehoshi): This works only with Go tools that assume all source files are in one directory.
// Fix this to work with other Go tools.
dir := filepath.Dir(pkg.GoFiles[0])
manifestPath := filepath.Join(dir, "AndroidManifest.xml")
manifestData, err := os.ReadFile(manifestPath)
if err != nil {
if !os.IsNotExist(err) {
return nil, err
}
buf := new(bytes.Buffer)
buf.WriteString(`<?xml version="1.0" encoding="utf-8"?>`)
err := manifestTmpl.Execute(buf, manifestTmplData{
// TODO(crawshaw): a better package path.
JavaPkgPath: "org.golang.todo." + libName,
Name: strings.Title(appName),
LibName: libName,
})
if err != nil {
return nil, err
}
manifestData = buf.Bytes()
if buildV {
fmt.Fprintf(os.Stderr, "generated AndroidManifest.xml:\n%s\n", manifestData)
}
} else {
libName, err = manifestLibName(manifestData)
if err != nil {
return nil, fmt.Errorf("error parsing %s: %v", manifestPath, err)
}
}
libFiles := []string{}
nmpkgs := make(map[string]map[string]bool) // map: arch -> extractPkgs' output
for _, t := range targets {
toolchain := ndk.Toolchain(t.arch)
libPath := "lib/" + toolchain.abi + "/lib" + libName + ".so"
libAbsPath := filepath.Join(tmpdir, libPath)
if err := mkdir(filepath.Dir(libAbsPath)); err != nil {
return nil, err
}
err = goBuild(
pkg.PkgPath,
androidEnv[t.arch],
"-buildmode=c-shared",
"-o", libAbsPath,
)
if err != nil {
return nil, err
}
nmpkgs[t.arch], err = extractPkgs(toolchain.Path(ndkRoot, "nm"), libAbsPath)
if err != nil {
return nil, err
}
libFiles = append(libFiles, libPath)
}
block, _ := pem.Decode([]byte(debugCert))
if block == nil {
return nil, errors.New("no debug cert")
}
privKey, err := x509.ParsePKCS1PrivateKey(block.Bytes)
if err != nil {
return nil, err
}
if buildO == "" {
buildO = androidPkgName(path.Base(pkg.PkgPath)) + ".apk"
}
if !strings.HasSuffix(buildO, ".apk") {
return nil, fmt.Errorf("output file name %q does not end in '.apk'", buildO)
}
var out io.Writer
if !buildN {
f, err := os.Create(buildO)
if err != nil {
return nil, err
}
defer func() {
if cerr := f.Close(); err == nil {
err = cerr
}
}()
out = f
}
var apkw *Writer
if !buildN {
apkw = NewWriter(out, privKey)
}
apkwCreate := func(name string) (io.Writer, error) {
if buildV {
fmt.Fprintf(os.Stderr, "apk: %s\n", name)
}
if buildN {
return io.Discard, nil
}
return apkw.Create(name)
}
apkwWriteFile := func(dst, src string) error {
w, err := apkwCreate(dst)
if err != nil {
return err
}
if !buildN {
f, err := os.Open(src)
if err != nil {
return err
}
defer f.Close()
if _, err := io.Copy(w, f); err != nil {
return err
}
}
return nil
}
w, err := apkwCreate("classes.dex")
if err != nil {
return nil, err
}
dexData, err := base64.StdEncoding.DecodeString(dexStr)
if err != nil {
log.Fatalf("internal error bad dexStr: %v", err)
}
if _, err := w.Write(dexData); err != nil {
return nil, err
}
for _, libFile := range libFiles {
if err := apkwWriteFile(libFile, filepath.Join(tmpdir, libFile)); err != nil {
return nil, err
}
}
for _, t := range targets {
toolchain := ndk.Toolchain(t.arch)
if nmpkgs[t.arch]["golang.org/x/mobile/exp/audio/al"] {
dst := "lib/" + toolchain.abi + "/libopenal.so"
src := filepath.Join(gomobilepath, dst)
if _, err := os.Stat(src); err != nil {
return nil, errors.New("the Android requires the golang.org/x/mobile/exp/audio/al, but the OpenAL libraries was not found. Please run gomobile init with the -openal flag pointing to an OpenAL source directory.")
}
if err := apkwWriteFile(dst, src); err != nil {
return nil, err
}
}
}
// Add any assets.
var arsc struct {
iconPath string
}
assetsDir := filepath.Join(dir, "assets")
assetsDirExists := true
fi, err := os.Stat(assetsDir)
if err != nil {
if os.IsNotExist(err) {
assetsDirExists = false
} else {
return nil, err
}
} else {
assetsDirExists = fi.IsDir()
}
if assetsDirExists {
// if assets is a symlink, follow the symlink.
assetsDir, err = filepath.EvalSymlinks(assetsDir)
if err != nil {
return nil, err
}
err = filepath.Walk(assetsDir, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if name := filepath.Base(path); strings.HasPrefix(name, ".") {
// Do not include the hidden files.
return nil
}
if info.IsDir() {
return nil
}
if rel, err := filepath.Rel(assetsDir, path); rel == "icon.png" && err == nil {
arsc.iconPath = path
// TODO returning here does not write the assets/icon.png to the final assets output,
// making it unavailable via the assets API. Should the file be duplicated into assets
// or should assets API be able to retrieve files from the generated resource table?
return nil
}
name := "assets/" + path[len(assetsDir)+1:]
return apkwWriteFile(name, path)
})
if err != nil {
return nil, fmt.Errorf("asset %v", err)
}
}
bxml, err := binres.UnmarshalXML(bytes.NewReader(manifestData), arsc.iconPath != "")
if err != nil {
return nil, err
}
// generate resources.arsc identifying single xxxhdpi icon resource.
if arsc.iconPath != "" {
pkgname, err := bxml.RawValueByName("manifest", xml.Name{Local: "package"})
if err != nil {
return nil, err
}
tbl, name := binres.NewMipmapTable(pkgname)
if err := apkwWriteFile(name, arsc.iconPath); err != nil {
return nil, err
}
w, err := apkwCreate("resources.arsc")
if err != nil {
return nil, err
}
bin, err := tbl.MarshalBinary()
if err != nil {
return nil, err
}
if _, err := w.Write(bin); err != nil {
return nil, err
}
}
w, err = apkwCreate("AndroidManifest.xml")
if err != nil {
return nil, err
}
bin, err := bxml.MarshalBinary()
if err != nil {
return nil, err
}
if _, err := w.Write(bin); err != nil {
return nil, err
}
// TODO: add gdbserver to apk?
if !buildN {
if err := apkw.Close(); err != nil {
return nil, err
}
}
// TODO: return nmpkgs
return nmpkgs[targets[0].arch], nil
}
// androidPkgName sanitizes the go package name to be acceptable as a android
// package name part. The android package name convention is similar to the
// java package name convention described in
// https://docs.oracle.com/javase/specs/jls/se8/html/jls-6.html#jls-6.5.3.1
// but not exactly same.
func androidPkgName(name string) string {
var res []rune
for _, r := range name {
switch {
case 'a' <= r && r <= 'z', 'A' <= r && r <= 'Z', '0' <= r && r <= '9':
res = append(res, r)
default:
res = append(res, '_')
}
}
if len(res) == 0 || res[0] == '_' || ('0' <= res[0] && res[0] <= '9') {
// Android does not seem to allow the package part starting with _.
res = append([]rune{'g', 'o'}, res...)
}
s := string(res)
// Look for Java keywords that are not Go keywords, and avoid using
// them as a package name.
//
// This is not a problem for normal Go identifiers as we only expose
// exported symbols. The upper case first letter saves everything
// from accidentally matching except for the package name.
//
// Note that basic type names (like int) are not keywords in Go.
switch s {
case "abstract", "assert", "boolean", "byte", "catch", "char", "class",
"do", "double", "enum", "extends", "final", "finally", "float",
"implements", "instanceof", "int", "long", "native", "private",
"protected", "public", "short", "static", "strictfp", "super",
"synchronized", "this", "throw", "throws", "transient", "try",
"void", "volatile", "while":
s += "_"
}
return s
}
// A random uninteresting private key.
// Must be consistent across builds so newer app versions can be installed.
const debugCert = `
-----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEAy6ItnWZJ8DpX9R5FdWbS9Kr1U8Z7mKgqNByGU7No99JUnmyu
NQ6Uy6Nj0Gz3o3c0BXESECblOC13WdzjsH1Pi7/L9QV8jXOXX8cvkG5SJAyj6hcO
LOapjDiN89NXjXtyv206JWYvRtpexyVrmHJgRAw3fiFI+m4g4Qop1CxcIF/EgYh7
rYrqh4wbCM1OGaCleQWaOCXxZGm+J5YNKQcWpjZRrDrb35IZmlT0bK46CXUKvCqK
x7YXHgfhC8ZsXCtsScKJVHs7gEsNxz7A0XoibFw6DoxtjKzUCktnT0w3wxdY7OTj
9AR8mobFlM9W3yirX8TtwekWhDNTYEu8dwwykwIDAQABAoIBAA2hjpIhvcNR9H9Z
BmdEecydAQ0ZlT5zy1dvrWI++UDVmIp+Ve8BSd6T0mOqV61elmHi3sWsBN4M1Rdz
3N38lW2SajG9q0fAvBpSOBHgAKmfGv3Ziz5gNmtHgeEXfZ3f7J95zVGhlHqWtY95
JsmuplkHxFMyITN6WcMWrhQg4A3enKLhJLlaGLJf9PeBrvVxHR1/txrfENd2iJBH
FmxVGILL09fIIktJvoScbzVOneeWXj5vJGzWVhB17DHBbANGvVPdD5f+k/s5aooh
hWAy/yLKocr294C4J+gkO5h2zjjjSGcmVHfrhlXQoEPX+iW1TGoF8BMtl4Llc+jw
lKWKfpECgYEA9C428Z6CvAn+KJ2yhbAtuRo41kkOVoiQPtlPeRYs91Pq4+NBlfKO
2nWLkyavVrLx4YQeCeaEU2Xoieo9msfLZGTVxgRlztylOUR+zz2FzDBYGicuUD3s
EqC0Wv7tiX6dumpWyOcVVLmR9aKlOUzA9xemzIsWUwL3PpyONhKSq7kCgYEA1X2F
f2jKjoOVzglhtuX4/SP9GxS4gRf9rOQ1Q8DzZhyH2LZ6Dnb1uEQvGhiqJTU8CXxb
7odI0fgyNXq425Nlxc1Tu0G38TtJhwrx7HWHuFcbI/QpRtDYLWil8Zr7Q3BT9rdh
moo4m937hLMvqOG9pyIbyjOEPK2WBCtKW5yabqsCgYEAu9DkUBr1Qf+Jr+IEU9I8
iRkDSMeusJ6gHMd32pJVCfRRQvIlG1oTyTMKpafmzBAd/rFpjYHynFdRcutqcShm
aJUq3QG68U9EAvWNeIhA5tr0mUEz3WKTt4xGzYsyWES8u4tZr3QXMzD9dOuinJ1N
+4EEumXtSPKKDG3M8Qh+KnkCgYBUEVSTYmF5EynXc2xOCGsuy5AsrNEmzJqxDUBI
SN/P0uZPmTOhJIkIIZlmrlW5xye4GIde+1jajeC/nG7U0EsgRAV31J4pWQ5QJigz
0+g419wxIUFryGuIHhBSfpP472+w1G+T2mAGSLh1fdYDq7jx6oWE7xpghn5vb9id
EKLjdwKBgBtz9mzbzutIfAW0Y8F23T60nKvQ0gibE92rnUbjPnw8HjL3AZLU05N+
cSL5bhq0N5XHK77sscxW9vXjG0LJMXmFZPp9F6aV6ejkMIXyJ/Yz/EqeaJFwilTq
Mc6xR47qkdzu0dQ1aPm4XD7AWDtIvPo/GG2DKOucLBbQc2cOWtKS
-----END RSA PRIVATE KEY-----
`

617
cmd/gomobile/build_apple.go Normal file
View file

@ -0,0 +1,617 @@
// 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.
package main
import (
"bytes"
"crypto/x509"
"encoding/pem"
"fmt"
"os"
"os/exec"
"path"
"path/filepath"
"strings"
"text/template"
"golang.org/x/tools/go/packages"
)
func goAppleBuild(pkg *packages.Package, bundleID string, targets []targetInfo) (map[string]bool, error) {
src := pkg.PkgPath
if buildO != "" && !strings.HasSuffix(buildO, ".app") {
return nil, fmt.Errorf("-o must have an .app for -target=ios")
}
productName := rfc1034Label(path.Base(pkg.PkgPath))
if productName == "" {
productName = "ProductName" // like xcode.
}
infoplist := new(bytes.Buffer)
if err := infoplistTmpl.Execute(infoplist, infoplistTmplData{
BundleID: bundleID + "." + productName,
Name: strings.Title(path.Base(pkg.PkgPath)),
}); err != nil {
return nil, err
}
// Detect the team ID
teamID, err := detectTeamID()
if err != nil {
return nil, err
}
projPbxproj := new(bytes.Buffer)
if err := projPbxprojTmpl.Execute(projPbxproj, projPbxprojTmplData{
TeamID: teamID,
}); err != nil {
return nil, err
}
files := []struct {
name string
contents []byte
}{
{tmpdir + "/main.xcodeproj/project.pbxproj", projPbxproj.Bytes()},
{tmpdir + "/main/Info.plist", infoplist.Bytes()},
{tmpdir + "/main/Images.xcassets/AppIcon.appiconset/Contents.json", []byte(contentsJSON)},
}
for _, file := range files {
if err := mkdir(filepath.Dir(file.name)); err != nil {
return nil, err
}
if buildX {
printcmd("echo \"%s\" > %s", file.contents, file.name)
}
if !buildN {
if err := os.WriteFile(file.name, file.contents, 0644); err != nil {
return nil, err
}
}
}
// We are using lipo tool to build multiarchitecture binaries.
cmd := exec.Command(
"xcrun", "lipo",
"-o", filepath.Join(tmpdir, "main/main"),
"-create",
)
var nmpkgs map[string]bool
builtArch := map[string]bool{}
for _, t := range targets {
// Only one binary per arch allowed
// e.g. ios/arm64 + iossimulator/amd64
if builtArch[t.arch] {
continue
}
builtArch[t.arch] = true
path := filepath.Join(tmpdir, t.platform, t.arch)
// Disable DWARF; see golang.org/issues/25148.
if err := goBuild(src, appleEnv[t.String()], "-ldflags=-w", "-o="+path); err != nil {
return nil, err
}
if nmpkgs == nil {
var err error
nmpkgs, err = extractPkgs(appleNM, path)
if err != nil {
return nil, err
}
}
cmd.Args = append(cmd.Args, path)
}
if err := runCmd(cmd); err != nil {
return nil, err
}
// TODO(jbd): Set the launcher icon.
if err := appleCopyAssets(pkg, tmpdir); err != nil {
return nil, err
}
// Build and move the release build to the output directory.
cmdStrings := []string{
"xcodebuild",
"-configuration", "Release",
"-project", tmpdir + "/main.xcodeproj",
"-allowProvisioningUpdates",
"DEVELOPMENT_TEAM=" + teamID,
}
cmd = exec.Command("xcrun", cmdStrings...)
if err := runCmd(cmd); err != nil {
return nil, err
}
// TODO(jbd): Fallback to copying if renaming fails.
if buildO == "" {
n := pkg.PkgPath
if n == "." {
// use cwd name
cwd, err := os.Getwd()
if err != nil {
return nil, fmt.Errorf("cannot create .app; cannot get the current working dir: %v", err)
}
n = cwd
}
n = path.Base(n)
buildO = n + ".app"
}
if buildX {
printcmd("mv %s %s", tmpdir+"/build/Release-iphoneos/main.app", buildO)
}
if !buildN {
// if output already exists, remove.
if err := os.RemoveAll(buildO); err != nil {
return nil, err
}
if err := os.Rename(tmpdir+"/build/Release-iphoneos/main.app", buildO); err != nil {
return nil, err
}
}
return nmpkgs, nil
}
func detectTeamID() (string, error) {
// Grabs the first certificate for "Apple Development"; will not work if there
// are multiple certificates and the first is not desired.
cmd := exec.Command(
"security", "find-certificate",
"-c", "Apple Development", "-p",
)
pemString, err := cmd.Output()
if err != nil {
err = fmt.Errorf("failed to pull the signing certificate to determine your team ID: %v", err)
return "", err
}
block, _ := pem.Decode(pemString)
if block == nil {
err = fmt.Errorf("failed to decode the PEM to determine your team ID: %s", pemString)
return "", err
}
cert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
err = fmt.Errorf("failed to parse your signing certificate to determine your team ID: %v", err)
return "", err
}
if len(cert.Subject.OrganizationalUnit) == 0 {
err = fmt.Errorf("the signing certificate has no organizational unit (team ID)")
return "", err
}
return cert.Subject.OrganizationalUnit[0], nil
}
func appleCopyAssets(pkg *packages.Package, xcodeProjDir string) error {
dstAssets := xcodeProjDir + "/main/assets"
if err := mkdir(dstAssets); err != nil {
return err
}
// TODO(hajimehoshi): This works only with Go tools that assume all source files are in one directory.
// Fix this to work with other Go tools.
srcAssets := filepath.Join(filepath.Dir(pkg.GoFiles[0]), "assets")
fi, err := os.Stat(srcAssets)
if err != nil {
if os.IsNotExist(err) {
// skip walking through the directory to deep copy.
return nil
}
return err
}
if !fi.IsDir() {
// skip walking through to deep copy.
return nil
}
// if assets is a symlink, follow the symlink.
srcAssets, err = filepath.EvalSymlinks(srcAssets)
if err != nil {
return err
}
return filepath.Walk(srcAssets, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if name := filepath.Base(path); strings.HasPrefix(name, ".") {
// Do not include the hidden files.
return nil
}
if info.IsDir() {
return nil
}
dst := dstAssets + "/" + path[len(srcAssets)+1:]
return copyFile(dst, path)
})
}
type infoplistTmplData struct {
BundleID string
Name string
}
var infoplistTmpl = template.Must(template.New("infoplist").Parse(`<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleExecutable</key>
<string>main</string>
<key>CFBundleIdentifier</key>
<string>{{.BundleID}}</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>{{.Name}}</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>1</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIRequiredDeviceCapabilities</key>
<array>
<string>armv7</string>
</array>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
</dict>
</plist>
`))
type projPbxprojTmplData struct {
TeamID string
}
var projPbxprojTmpl = template.Must(template.New("projPbxproj").Parse(`// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 46;
objects = {
/* Begin PBXBuildFile section */
254BB84F1B1FD08900C56DE9 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 254BB84E1B1FD08900C56DE9 /* Images.xcassets */; };
254BB8681B1FD16500C56DE9 /* main in Resources */ = {isa = PBXBuildFile; fileRef = 254BB8671B1FD16500C56DE9 /* main */; };
25FB30331B30FDEE0005924C /* assets in Resources */ = {isa = PBXBuildFile; fileRef = 25FB30321B30FDEE0005924C /* assets */; };
/* End PBXBuildFile section */
/* Begin PBXFileReference section */
254BB83E1B1FD08900C56DE9 /* main.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = main.app; sourceTree = BUILT_PRODUCTS_DIR; };
254BB8421B1FD08900C56DE9 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
254BB84E1B1FD08900C56DE9 /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = "<group>"; };
254BB8671B1FD16500C56DE9 /* main */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.executable"; path = main; sourceTree = "<group>"; };
25FB30321B30FDEE0005924C /* assets */ = {isa = PBXFileReference; lastKnownFileType = folder; name = assets; path = main/assets; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXGroup section */
254BB8351B1FD08900C56DE9 = {
isa = PBXGroup;
children = (
25FB30321B30FDEE0005924C /* assets */,
254BB8401B1FD08900C56DE9 /* main */,
254BB83F1B1FD08900C56DE9 /* Products */,
);
sourceTree = "<group>";
usesTabs = 0;
};
254BB83F1B1FD08900C56DE9 /* Products */ = {
isa = PBXGroup;
children = (
254BB83E1B1FD08900C56DE9 /* main.app */,
);
name = Products;
sourceTree = "<group>";
};
254BB8401B1FD08900C56DE9 /* main */ = {
isa = PBXGroup;
children = (
254BB8671B1FD16500C56DE9 /* main */,
254BB84E1B1FD08900C56DE9 /* Images.xcassets */,
254BB8411B1FD08900C56DE9 /* Supporting Files */,
);
path = main;
sourceTree = "<group>";
};
254BB8411B1FD08900C56DE9 /* Supporting Files */ = {
isa = PBXGroup;
children = (
254BB8421B1FD08900C56DE9 /* Info.plist */,
);
name = "Supporting Files";
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
254BB83D1B1FD08900C56DE9 /* main */ = {
isa = PBXNativeTarget;
buildConfigurationList = 254BB8611B1FD08900C56DE9 /* Build configuration list for PBXNativeTarget "main" */;
buildPhases = (
254BB83C1B1FD08900C56DE9 /* Resources */,
);
buildRules = (
);
dependencies = (
);
name = main;
productName = main;
productReference = 254BB83E1B1FD08900C56DE9 /* main.app */;
productType = "com.apple.product-type.application";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
254BB8361B1FD08900C56DE9 /* Project object */ = {
isa = PBXProject;
attributes = {
LastUpgradeCheck = 0630;
ORGANIZATIONNAME = Developer;
TargetAttributes = {
254BB83D1B1FD08900C56DE9 = {
CreatedOnToolsVersion = 6.3.1;
DevelopmentTeam = {{.TeamID}};
};
};
};
buildConfigurationList = 254BB8391B1FD08900C56DE9 /* Build configuration list for PBXProject "main" */;
compatibilityVersion = "Xcode 3.2";
developmentRegion = English;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
mainGroup = 254BB8351B1FD08900C56DE9;
productRefGroup = 254BB83F1B1FD08900C56DE9 /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
254BB83D1B1FD08900C56DE9 /* main */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
254BB83C1B1FD08900C56DE9 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
25FB30331B30FDEE0005924C /* assets in Resources */,
254BB8681B1FD16500C56DE9 /* main in Resources */,
254BB84F1B1FD08900C56DE9 /* Images.xcassets in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin XCBuildConfiguration section */
254BB8601B1FD08900C56DE9 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Development";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";
VALIDATE_PRODUCT = YES;
ENABLE_BITCODE = YES;
};
name = Release;
};
254BB8631B1FD08900C56DE9 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
INFOPLIST_FILE = main/Info.plist;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
PRODUCT_NAME = "$(TARGET_NAME)";
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
254BB8391B1FD08900C56DE9 /* Build configuration list for PBXProject "main" */ = {
isa = XCConfigurationList;
buildConfigurations = (
254BB8601B1FD08900C56DE9 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
254BB8611B1FD08900C56DE9 /* Build configuration list for PBXNativeTarget "main" */ = {
isa = XCConfigurationList;
buildConfigurations = (
254BB8631B1FD08900C56DE9 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
};
rootObject = 254BB8361B1FD08900C56DE9 /* Project object */;
}
`))
const contentsJSON = `{
"images" : [
{
"idiom" : "iphone",
"size" : "29x29",
"scale" : "2x"
},
{
"idiom" : "iphone",
"size" : "29x29",
"scale" : "3x"
},
{
"idiom" : "iphone",
"size" : "40x40",
"scale" : "2x"
},
{
"idiom" : "iphone",
"size" : "40x40",
"scale" : "3x"
},
{
"idiom" : "iphone",
"size" : "60x60",
"scale" : "2x"
},
{
"idiom" : "iphone",
"size" : "60x60",
"scale" : "3x"
},
{
"idiom" : "ipad",
"size" : "29x29",
"scale" : "1x"
},
{
"idiom" : "ipad",
"size" : "29x29",
"scale" : "2x"
},
{
"idiom" : "ipad",
"size" : "40x40",
"scale" : "1x"
},
{
"idiom" : "ipad",
"size" : "40x40",
"scale" : "2x"
},
{
"idiom" : "ipad",
"size" : "76x76",
"scale" : "1x"
},
{
"idiom" : "ipad",
"size" : "76x76",
"scale" : "2x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}
`
// rfc1034Label sanitizes the name to be usable in a uniform type identifier.
// The sanitization is similar to xcode's rfc1034identifier macro that
// replaces illegal characters (not conforming the rfc1034 label rule) with '-'.
func rfc1034Label(name string) string {
// * Uniform type identifier:
//
// According to
// https://developer.apple.com/library/ios/documentation/FileManagement/Conceptual/understanding_utis/understand_utis_conc/understand_utis_conc.html
//
// A uniform type identifier is a Unicode string that usually contains characters
// in the ASCII character set. However, only a subset of the ASCII characters are
// permitted. You may use the Roman alphabet in upper and lower case (AZ, az),
// the digits 0 through 9, the dot (“.”), and the hyphen (“-”). This restriction
// is based on DNS name restrictions, set forth in RFC 1035.
//
// Uniform type identifiers may also contain any of the Unicode characters greater
// than U+007F.
//
// Note: the actual implementation of xcode does not allow some unicode characters
// greater than U+007f. In this implementation, we just replace everything non
// alphanumeric with "-" like the rfc1034identifier macro.
//
// * RFC1034 Label
//
// <label> ::= <letter> [ [ <ldh-str> ] <let-dig> ]
// <ldh-str> ::= <let-dig-hyp> | <let-dig-hyp> <ldh-str>
// <let-dig-hyp> ::= <let-dig> | "-"
// <let-dig> ::= <letter> | <digit>
const surrSelf = 0x10000
begin := false
var res []rune
for i, r := range name {
if r == '.' && !begin {
continue
}
begin = true
switch {
case 'a' <= r && r <= 'z', 'A' <= r && r <= 'Z':
res = append(res, r)
case '0' <= r && r <= '9':
if i == 0 {
res = append(res, '-')
} else {
res = append(res, r)
}
default:
if r < surrSelf {
res = append(res, '-')
} else {
res = append(res, '-', '-')
}
}
}
return string(res)
}

View file

@ -0,0 +1,123 @@
// 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.
package main
import (
"bytes"
"os"
"path/filepath"
"testing"
"text/template"
)
func TestAppleBuild(t *testing.T) {
if !xcodeAvailable() {
t.Skip("Xcode is missing")
}
defer func() {
xout = os.Stderr
buildN = false
buildX = false
}()
buildN = true
buildX = true
buildTarget = "ios"
buildBundleID = "org.golang.todo"
gopath = filepath.SplitList(goEnv("GOPATH"))[0]
oldTags := buildTags
buildTags = []string{"tag1"}
defer func() {
buildTags = oldTags
}()
tests := []struct {
pkg string
main bool
}{
{"golang.org/x/mobile/example/basic", true},
{"golang.org/x/mobile/bind/testdata/testpkg", false},
}
for _, test := range tests {
buf := new(bytes.Buffer)
xout = buf
var tmpl *template.Template
if test.main {
buildO = "basic.app"
tmpl = appleMainBuildTmpl
} else {
buildO = ""
tmpl = appleOtherBuildTmpl
}
cmdBuild.flag.Parse([]string{test.pkg})
err := runBuild(cmdBuild)
if err != nil {
t.Log(buf.String())
t.Fatal(err)
}
teamID, err := detectTeamID()
if err != nil {
t.Fatalf("detecting team ID failed: %v", err)
}
output, err := defaultOutputData(teamID)
if err != nil {
t.Fatal(err)
}
data := struct {
outputData
TeamID string
Pkg string
Main bool
BuildO string
}{
outputData: output,
TeamID: teamID,
Pkg: test.pkg,
Main: test.main,
BuildO: buildO,
}
got := filepath.ToSlash(buf.String())
wantBuf := new(bytes.Buffer)
if err := tmpl.Execute(wantBuf, data); err != nil {
t.Fatalf("computing diff failed: %v", err)
}
diff, err := diff(got, wantBuf.String())
if err != nil {
t.Fatalf("computing diff failed: %v", err)
}
if diff != "" {
t.Errorf("unexpected output:\n%s", diff)
}
}
}
var appleMainBuildTmpl = template.Must(infoplistTmpl.New("output").Parse(`GOMOBILE={{.GOPATH}}/pkg/gomobile
WORK=$WORK
mkdir -p $WORK/main.xcodeproj
echo "{{.Xproj}}" > $WORK/main.xcodeproj/project.pbxproj
mkdir -p $WORK/main
echo "{{template "infoplist" .Xinfo}}" > $WORK/main/Info.plist
mkdir -p $WORK/main/Images.xcassets/AppIcon.appiconset
echo "{{.Xcontents}}" > $WORK/main/Images.xcassets/AppIcon.appiconset/Contents.json
GOMODCACHE=$GOPATH/pkg/mod GOOS=ios GOARCH=arm64 GOFLAGS=-tags=ios CC=iphoneos-clang CXX=iphoneos-clang++ CGO_CFLAGS=-isysroot iphoneos -miphoneos-version-min=13.0 -fembed-bitcode -arch arm64 CGO_CXXFLAGS=-isysroot iphoneos -miphoneos-version-min=13.0 -fembed-bitcode -arch arm64 CGO_LDFLAGS=-isysroot iphoneos -miphoneos-version-min=13.0 -fembed-bitcode -arch arm64 CGO_ENABLED=1 DARWIN_SDK=iphoneos go build -tags tag1 -x -ldflags=-w -o=$WORK/ios/arm64 {{.Pkg}}
GOMODCACHE=$GOPATH/pkg/mod GOOS=ios GOARCH=amd64 GOFLAGS=-tags=ios CC=iphonesimulator-clang CXX=iphonesimulator-clang++ CGO_CFLAGS=-isysroot iphonesimulator -mios-simulator-version-min=13.0 -fembed-bitcode -arch x86_64 CGO_CXXFLAGS=-isysroot iphonesimulator -mios-simulator-version-min=13.0 -fembed-bitcode -arch x86_64 CGO_LDFLAGS=-isysroot iphonesimulator -mios-simulator-version-min=13.0 -fembed-bitcode -arch x86_64 CGO_ENABLED=1 DARWIN_SDK=iphonesimulator go build -tags tag1 -x -ldflags=-w -o=$WORK/iossimulator/amd64 {{.Pkg}}
xcrun lipo -o $WORK/main/main -create $WORK/ios/arm64 $WORK/iossimulator/amd64
mkdir -p $WORK/main/assets
xcrun xcodebuild -configuration Release -project $WORK/main.xcodeproj -allowProvisioningUpdates DEVELOPMENT_TEAM={{.TeamID}}
mv $WORK/build/Release-iphoneos/main.app {{.BuildO}}
`))
var appleOtherBuildTmpl = template.Must(infoplistTmpl.New("output").Parse(`GOMOBILE={{.GOPATH}}/pkg/gomobile
WORK=$WORK
GOMODCACHE=$GOPATH/pkg/mod GOOS=ios GOARCH=arm64 GOFLAGS=-tags=ios CC=iphoneos-clang CXX=iphoneos-clang++ CGO_CFLAGS=-isysroot iphoneos -miphoneos-version-min=13.0 -fembed-bitcode -arch arm64 CGO_CXXFLAGS=-isysroot iphoneos -miphoneos-version-min=13.0 -fembed-bitcode -arch arm64 CGO_LDFLAGS=-isysroot iphoneos -miphoneos-version-min=13.0 -fembed-bitcode -arch arm64 CGO_ENABLED=1 DARWIN_SDK=iphoneos go build -tags tag1 -x {{.Pkg}}
GOMODCACHE=$GOPATH/pkg/mod GOOS=ios GOARCH=arm64 GOFLAGS=-tags=ios CC=iphonesimulator-clang CXX=iphonesimulator-clang++ CGO_CFLAGS=-isysroot iphonesimulator -mios-simulator-version-min=13.0 -fembed-bitcode -arch arm64 CGO_CXXFLAGS=-isysroot iphonesimulator -mios-simulator-version-min=13.0 -fembed-bitcode -arch arm64 CGO_LDFLAGS=-isysroot iphonesimulator -mios-simulator-version-min=13.0 -fembed-bitcode -arch arm64 CGO_ENABLED=1 DARWIN_SDK=iphonesimulator go build -tags tag1 -x {{.Pkg}}
GOMODCACHE=$GOPATH/pkg/mod GOOS=ios GOARCH=amd64 GOFLAGS=-tags=ios CC=iphonesimulator-clang CXX=iphonesimulator-clang++ CGO_CFLAGS=-isysroot iphonesimulator -mios-simulator-version-min=13.0 -fembed-bitcode -arch x86_64 CGO_CXXFLAGS=-isysroot iphonesimulator -mios-simulator-version-min=13.0 -fembed-bitcode -arch x86_64 CGO_LDFLAGS=-isysroot iphonesimulator -mios-simulator-version-min=13.0 -fembed-bitcode -arch x86_64 CGO_ENABLED=1 DARWIN_SDK=iphonesimulator go build -tags tag1 -x {{.Pkg}}
`))

283
cmd/gomobile/build_test.go Normal file
View file

@ -0,0 +1,283 @@
// 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.
package main
import (
"bytes"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"testing"
"text/template"
"golang.org/x/mobile/internal/sdkpath"
)
func TestRFC1034Label(t *testing.T) {
tests := []struct {
in, want string
}{
{"a", "a"},
{"123", "-23"},
{"a.b.c", "a-b-c"},
{"a-b", "a-b"},
{"a:b", "a-b"},
{"a?b", "a-b"},
{"αβγ", "---"},
{"💩", "--"},
{"My App", "My-App"},
{"...", ""},
{".-.", "--"},
}
for _, tc := range tests {
if got := rfc1034Label(tc.in); got != tc.want {
t.Errorf("rfc1034Label(%q) = %q, want %q", tc.in, got, tc.want)
}
}
}
func TestAndroidPkgName(t *testing.T) {
tests := []struct {
in, want string
}{
{"a", "a"},
{"a123", "a123"},
{"a.b.c", "a_b_c"},
{"a-b", "a_b"},
{"a:b", "a_b"},
{"a?b", "a_b"},
{"αβγ", "go___"},
{"💩", "go_"},
{"My App", "My_App"},
{"...", "go___"},
{".-.", "go___"},
{"abstract", "abstract_"},
{"Abstract", "Abstract"},
{"12345", "go12345"},
}
for _, tc := range tests {
if got := androidPkgName(tc.in); got != tc.want {
t.Errorf("len %d", len(tc.in))
t.Errorf("androidPkgName(%q) = %q, want %q", tc.in, got, tc.want)
}
}
}
func TestAndroidBuild(t *testing.T) {
if runtime.GOOS == "android" || runtime.GOOS == "ios" {
t.Skipf("not available on %s", runtime.GOOS)
}
buf := new(bytes.Buffer)
defer func() {
xout = os.Stderr
buildN = false
buildX = false
}()
xout = buf
buildN = true
buildX = true
buildO = "basic.apk"
buildTarget = "android/arm"
gopath = filepath.ToSlash(filepath.SplitList(goEnv("GOPATH"))[0])
if goos == "windows" {
os.Setenv("HOMEDRIVE", "C:")
}
cmdBuild.flag.Parse([]string{"golang.org/x/mobile/example/basic"})
oldTags := buildTags
buildTags = []string{"tag1"}
defer func() {
buildTags = oldTags
}()
err := runBuild(cmdBuild)
if err != nil {
t.Log(buf.String())
t.Fatal(err)
}
diff, err := diffOutput(buf.String(), androidBuildTmpl)
if err != nil {
t.Fatalf("computing diff failed: %v", err)
}
if diff != "" {
t.Errorf("unexpected output:\n%s", diff)
}
}
var androidBuildTmpl = template.Must(template.New("output").Parse(`GOMOBILE={{.GOPATH}}/pkg/gomobile
WORK=$WORK
mkdir -p $WORK/lib/armeabi-v7a
GOMODCACHE=$GOPATH/pkg/mod GOOS=android GOARCH=arm CC=$NDK_PATH/toolchains/llvm/prebuilt/{{.NDKARCH}}/bin/armv7a-linux-androideabi16-clang CXX=$NDK_PATH/toolchains/llvm/prebuilt/{{.NDKARCH}}/bin/armv7a-linux-androideabi16-clang++ CGO_ENABLED=1 GOARM=7 go build -tags tag1 -x -buildmode=c-shared -o $WORK/lib/armeabi-v7a/libbasic.so golang.org/x/mobile/example/basic
`))
func TestParseBuildTarget(t *testing.T) {
wantAndroid := "android/" + strings.Join(platformArchs("android"), ",android/")
tests := []struct {
in string
wantErr bool
want string
}{
{"android", false, wantAndroid},
{"android,android/arm", false, wantAndroid},
{"android/arm", false, "android/arm"},
{"ios", false, "ios/arm64,iossimulator/arm64,iossimulator/amd64"},
{"ios,ios/arm64", false, "ios/arm64"},
{"ios/arm64", false, "ios/arm64"},
{"iossimulator", false, "iossimulator/arm64,iossimulator/amd64"},
{"iossimulator/amd64", false, "iossimulator/amd64"},
{"macos", false, "macos/arm64,macos/amd64"},
{"macos,ios/arm64", false, "macos/arm64,macos/amd64,ios/arm64"},
{"macos/arm64", false, "macos/arm64"},
{"macos/amd64", false, "macos/amd64"},
{"maccatalyst", false, "maccatalyst/arm64,maccatalyst/amd64"},
{"maccatalyst,ios/arm64", false, "maccatalyst/arm64,maccatalyst/amd64,ios/arm64"},
{"maccatalyst/arm64", false, "maccatalyst/arm64"},
{"maccatalyst/amd64", false, "maccatalyst/amd64"},
{"", true, ""},
{"linux", true, ""},
{"android/x86", true, ""},
{"android/arm5", true, ""},
{"ios/mips", true, ""},
{"android,ios", true, ""},
{"ios,android", true, ""},
{"ios/amd64", true, ""},
}
for _, tc := range tests {
t.Run(tc.in, func(t *testing.T) {
targets, err := parseBuildTarget(tc.in)
var s []string
for _, t := range targets {
s = append(s, t.String())
}
got := strings.Join(s, ",")
if tc.wantErr {
if err == nil {
t.Errorf("-target=%q; want error, got (%q, nil)", tc.in, got)
}
return
}
if err != nil || got != tc.want {
t.Errorf("-target=%q; want (%q, nil), got (%q, %v)", tc.in, tc.want, got, err)
}
})
}
}
func TestRegexImportGolangXPackage(t *testing.T) {
tests := []struct {
in string
want string
wantLen int
}{
{"ffffffff t golang.org/x/mobile", "golang.org/x/mobile", 2},
{"ffffffff t github.com/example/repo/vendor/golang.org/x/mobile", "golang.org/x/mobile", 2},
{"ffffffff t github.com/example/golang.org/x/mobile", "", 0},
{"ffffffff t github.com/example/repo", "", 0},
{"ffffffff t github.com/example/repo/vendor", "", 0},
{"ffffffff t _golang.org/x/mobile/app", "golang.org/x/mobile/app", 2},
}
for _, tc := range tests {
res := nmRE.FindStringSubmatch(tc.in)
if len(res) != tc.wantLen {
t.Errorf("nmRE returned unexpected result for %q: want len(res) = %d, got %d",
tc.in, tc.wantLen, len(res))
continue
}
if tc.wantLen == 0 {
continue
}
if res[1] != tc.want {
t.Errorf("nmRE returned unexpected result. want (%v), got (%v)",
tc.want, res[1])
}
}
}
func TestBuildWithGoModules(t *testing.T) {
if runtime.GOOS == "android" || runtime.GOOS == "ios" {
t.Skipf("gomobile are not available on %s", runtime.GOOS)
}
dir, err := os.MkdirTemp("", "gomobile-test")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(dir)
if out, err := exec.Command("go", "build", "-o="+dir, "golang.org/x/mobile/cmd/gomobile").CombinedOutput(); err != nil {
t.Fatalf("%v: %s", err, string(out))
}
path := dir
if p := os.Getenv("PATH"); p != "" {
path += string(filepath.ListSeparator) + p
}
for _, target := range []string{"android", "ios"} {
t.Run(target, func(t *testing.T) {
switch target {
case "android":
if _, err := sdkpath.AndroidAPIPath(minAndroidAPI); err != nil {
t.Skip("No compatible android API platform found, skipping bind")
}
case "ios":
if !xcodeAvailable() {
t.Skip("Xcode is missing")
}
}
var out string
switch target {
case "android":
out = filepath.Join(dir, "basic.apk")
case "ios":
out = filepath.Join(dir, "Basic.app")
}
tests := []struct {
Name string
Path string
Dir string
}{
{
Name: "Absolute Path",
Path: "golang.org/x/mobile/example/basic",
},
{
Name: "Relative Path",
Path: "./example/basic",
Dir: filepath.Join("..", ".."),
},
}
for _, tc := range tests {
tc := tc
t.Run(tc.Name, func(t *testing.T) {
args := []string{"build", "-target=" + target, "-o=" + out}
if target == "ios" {
args = append(args, "-bundleid=org.golang.gomobiletest")
}
args = append(args, tc.Path)
cmd := exec.Command(filepath.Join(dir, "gomobile"), args...)
cmd.Env = append(os.Environ(), "PATH="+path, "GO111MODULE=on")
cmd.Dir = tc.Dir
if out, err := cmd.CombinedOutput(); err != nil {
t.Errorf("gomobile build failed: %v\n%s", err, string(out))
}
})
}
})
}
}

154
cmd/gomobile/cert.go Normal file
View file

@ -0,0 +1,154 @@
// 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.
package main
import (
"crypto"
"crypto/rsa"
"crypto/sha1"
"crypto/x509"
"crypto/x509/pkix"
"encoding/asn1"
"io"
"math/big"
"time"
)
// signPKCS7 does the minimal amount of work necessary to embed an RSA
// signature into a PKCS#7 certificate.
//
// We prepare the certificate using the x509 package, read it back in
// to our custom data type and then write it back out with the signature.
func signPKCS7(rand io.Reader, priv *rsa.PrivateKey, msg []byte) ([]byte, error) {
const serialNumber = 0x5462c4dd // arbitrary
name := pkix.Name{CommonName: "gomobile"}
template := &x509.Certificate{
SerialNumber: big.NewInt(serialNumber),
SignatureAlgorithm: x509.SHA1WithRSA,
Subject: name,
}
b, err := x509.CreateCertificate(rand, template, template, priv.Public(), priv)
if err != nil {
return nil, err
}
c := certificate{}
if _, err := asn1.Unmarshal(b, &c); err != nil {
return nil, err
}
h := sha1.New()
h.Write(msg)
hashed := h.Sum(nil)
signed, err := rsa.SignPKCS1v15(rand, priv, crypto.SHA1, hashed)
if err != nil {
return nil, err
}
content := pkcs7SignedData{
ContentType: oidSignedData,
Content: signedData{
Version: 1,
DigestAlgorithms: []pkix.AlgorithmIdentifier{{
Algorithm: oidSHA1,
Parameters: asn1.RawValue{Tag: 5},
}},
ContentInfo: contentInfo{Type: oidData},
Certificates: c,
SignerInfos: []signerInfo{{
Version: 1,
IssuerAndSerialNumber: issuerAndSerialNumber{
Issuer: name.ToRDNSequence(),
SerialNumber: serialNumber,
},
DigestAlgorithm: pkix.AlgorithmIdentifier{
Algorithm: oidSHA1,
Parameters: asn1.RawValue{Tag: 5},
},
DigestEncryptionAlgorithm: pkix.AlgorithmIdentifier{
Algorithm: oidRSAEncryption,
Parameters: asn1.RawValue{Tag: 5},
},
EncryptedDigest: signed,
}},
},
}
return asn1.Marshal(content)
}
type pkcs7SignedData struct {
ContentType asn1.ObjectIdentifier
Content signedData `asn1:"tag:0,explicit"`
}
// signedData is defined in rfc2315, section 9.1.
type signedData struct {
Version int
DigestAlgorithms []pkix.AlgorithmIdentifier `asn1:"set"`
ContentInfo contentInfo
Certificates certificate `asn1:"tag0,explicit"`
SignerInfos []signerInfo `asn1:"set"`
}
type contentInfo struct {
Type asn1.ObjectIdentifier
// Content is optional in PKCS#7 and not provided here.
}
// certificate is defined in rfc2459, section 4.1.
type certificate struct {
TBSCertificate tbsCertificate
SignatureAlgorithm pkix.AlgorithmIdentifier
SignatureValue asn1.BitString
}
// tbsCertificate is defined in rfc2459, section 4.1.
type tbsCertificate struct {
Version int `asn1:"tag:0,default:2,explicit"`
SerialNumber int
Signature pkix.AlgorithmIdentifier
Issuer pkix.RDNSequence // pkix.Name
Validity validity
Subject pkix.RDNSequence // pkix.Name
SubjectPKI subjectPublicKeyInfo
}
// validity is defined in rfc2459, section 4.1.
type validity struct {
NotBefore time.Time
NotAfter time.Time
}
// subjectPublicKeyInfo is defined in rfc2459, section 4.1.
type subjectPublicKeyInfo struct {
Algorithm pkix.AlgorithmIdentifier
SubjectPublicKey asn1.BitString
}
type signerInfo struct {
Version int
IssuerAndSerialNumber issuerAndSerialNumber
DigestAlgorithm pkix.AlgorithmIdentifier
DigestEncryptionAlgorithm pkix.AlgorithmIdentifier
EncryptedDigest []byte
}
type issuerAndSerialNumber struct {
Issuer pkix.RDNSequence // pkix.Name
SerialNumber int
}
// Various ASN.1 Object Identifies, mostly from rfc3852.
var (
oidPKCS7 = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 7}
oidData = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 7, 1}
oidSignedData = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 7, 2}
oidSHA1 = asn1.ObjectIdentifier{1, 3, 14, 3, 2, 26}
oidRSAEncryption = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 1, 1}
)

101
cmd/gomobile/cert_test.go Normal file
View file

@ -0,0 +1,101 @@
// 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.
package main
import (
"crypto/rand"
"crypto/x509"
"encoding/pem"
"os"
"os/exec"
"testing"
)
func TestSignPKCS7(t *testing.T) {
// Setup RSA key.
block, _ := pem.Decode([]byte(testKey))
if block == nil {
t.Fatal("no cert")
}
privKey, err := x509.ParsePKCS1PrivateKey(block.Bytes)
if err != nil {
t.Fatal(err)
}
content := "Hello world,\nThis is signed."
cert, err := signPKCS7(rand.Reader, privKey, []byte(content))
if err != nil {
t.Fatal(err)
}
sig, err := os.CreateTemp("", "content.rsa")
if err != nil {
t.Fatal(err)
}
sigPath := sig.Name()
defer os.Remove(sigPath)
if _, err := sig.Write(cert); err != nil {
t.Fatal(err)
}
if err := sig.Close(); err != nil {
t.Fatal(err)
}
if openssl, err := exec.LookPath("openssl"); err != nil {
t.Log("command openssl not found, skipping")
} else {
cmd := exec.Command(
openssl, "asn1parse",
"-inform", "DER",
"-i",
"-in", sigPath,
)
if err := cmd.Run(); err != nil {
t.Errorf("bad asn.1: %v", err)
}
}
if keytool, err := exec.LookPath("keytool"); err != nil {
t.Log("command keytool not found, skipping")
} else if err := exec.Command(keytool, "-v").Run(); err != nil {
t.Logf("command keytool not functioning: %s, skipping", err)
} else {
cmd := exec.Command(keytool, "-v", "-printcert", "-file", sigPath)
out, err := cmd.CombinedOutput()
t.Logf("%v:\n%s", cmd.Args, out)
if err != nil {
t.Errorf("keytool cannot parse signature: %v", err)
}
}
}
const testKey = `
-----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEAy6ItnWZJ8DpX9R5FdWbS9Kr1U8Z7mKgqNByGU7No99JUnmyu
NQ6Uy6Nj0Gz3o3c0BXESECblOC13WdzjsH1Pi7/L9QV8jXOXX8cvkG5SJAyj6hcO
LOapjDiN89NXjXtyv206JWYvRtpexyVrmHJgRAw3fiFI+m4g4Qop1CxcIF/EgYh7
rYrqh4wbCM1OGaCleQWaOCXxZGm+J5YNKQcWpjZRrDrb35IZmlT0bK46CXUKvCqK
x7YXHgfhC8ZsXCtsScKJVHs7gEsNxz7A0XoibFw6DoxtjKzUCktnT0w3wxdY7OTj
9AR8mobFlM9W3yirX8TtwekWhDNTYEu8dwwykwIDAQABAoIBAA2hjpIhvcNR9H9Z
BmdEecydAQ0ZlT5zy1dvrWI++UDVmIp+Ve8BSd6T0mOqV61elmHi3sWsBN4M1Rdz
3N38lW2SajG9q0fAvBpSOBHgAKmfGv3Ziz5gNmtHgeEXfZ3f7J95zVGhlHqWtY95
JsmuplkHxFMyITN6WcMWrhQg4A3enKLhJLlaGLJf9PeBrvVxHR1/txrfENd2iJBH
FmxVGILL09fIIktJvoScbzVOneeWXj5vJGzWVhB17DHBbANGvVPdD5f+k/s5aooh
hWAy/yLKocr294C4J+gkO5h2zjjjSGcmVHfrhlXQoEPX+iW1TGoF8BMtl4Llc+jw
lKWKfpECgYEA9C428Z6CvAn+KJ2yhbAtuRo41kkOVoiQPtlPeRYs91Pq4+NBlfKO
2nWLkyavVrLx4YQeCeaEU2Xoieo9msfLZGTVxgRlztylOUR+zz2FzDBYGicuUD3s
EqC0Wv7tiX6dumpWyOcVVLmR9aKlOUzA9xemzIsWUwL3PpyONhKSq7kCgYEA1X2F
f2jKjoOVzglhtuX4/SP9GxS4gRf9rOQ1Q8DzZhyH2LZ6Dnb1uEQvGhiqJTU8CXxb
7odI0fgyNXq425Nlxc1Tu0G38TtJhwrx7HWHuFcbI/QpRtDYLWil8Zr7Q3BT9rdh
moo4m937hLMvqOG9pyIbyjOEPK2WBCtKW5yabqsCgYEAu9DkUBr1Qf+Jr+IEU9I8
iRkDSMeusJ6gHMd32pJVCfRRQvIlG1oTyTMKpafmzBAd/rFpjYHynFdRcutqcShm
aJUq3QG68U9EAvWNeIhA5tr0mUEz3WKTt4xGzYsyWES8u4tZr3QXMzD9dOuinJ1N
+4EEumXtSPKKDG3M8Qh+KnkCgYBUEVSTYmF5EynXc2xOCGsuy5AsrNEmzJqxDUBI
SN/P0uZPmTOhJIkIIZlmrlW5xye4GIde+1jajeC/nG7U0EsgRAV31J4pWQ5QJigz
0+g419wxIUFryGuIHhBSfpP472+w1G+T2mAGSLh1fdYDq7jx6oWE7xpghn5vb9id
EKLjdwKBgBtz9mzbzutIfAW0Y8F23T60nKvQ0gibE92rnUbjPnw8HjL3AZLU05N+
cSL5bhq0N5XHK77sscxW9vXjG0LJMXmFZPp9F6aV6ejkMIXyJ/Yz/EqeaJFwilTq
Mc6xR47qkdzu0dQ1aPm4XD7AWDtIvPo/GG2DKOucLBbQc2cOWtKS
-----END RSA PRIVATE KEY-----
`

32
cmd/gomobile/clean.go Normal file
View file

@ -0,0 +1,32 @@
// 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.
package main
import (
"fmt"
"path/filepath"
)
var cmdClean = &command{
run: runClean,
Name: "clean",
Usage: "",
Short: "remove object files and cached gomobile files",
Long: `
Clean removes object files and cached NDK files downloaded by gomobile init
`,
}
func runClean(cmd *command) (err error) {
gopaths := filepath.SplitList(goEnv("GOPATH"))
if len(gopaths) == 0 {
return fmt.Errorf("GOPATH is not set")
}
gomobilepath = filepath.Join(gopaths[0], "pkg/gomobile")
if buildX {
fmt.Fprintln(xout, "GOMOBILE="+gomobilepath)
}
return removeAll(gomobilepath)
}

51
cmd/gomobile/dex.go Normal file
View file

@ -0,0 +1,51 @@
// 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.
// Code generated by gendex.go. DO NOT EDIT.
package main
var dexStr = `ZGV4CjAzNQAoeJK42wHKtxuGg5UGeZAqfLMlHcB49+eQCAAAcAAAAHhWNBIAAAAAAAAAAP` +
`AHAAAxAAAAcAAAABEAAAA0AQAADwAAAHgBAAACAAAALAIAABMAAAA8AgAAAQAAANQCAACc` +
`BQAA9AIAAHIEAAB6BAAAfgQAAJUEAACYBAAAnQQAAKMEAACoBAAArgQAALEEAAC1BAAAuQ` +
`QAAL4EAADcBAAA/QQAABcFAAA6BQAAXwUAAHQFAACIBQAAvQUAAN0FAADtBQAABAYAABgG` +
`AAAsBgAAQwYAAGYGAABpBgAAbQYAAIMGAACGBgAAqQYAAK4GAAC/BgAA0AYAAN0GAADrBg` +
`AA9gYAAAkHAAASBwAAHQcAACgHAAA6BwAAQAcAAE0HAABhBwAAigcAAJQHAAADAAAADAAA` +
`AA0AAAAOAAAADwAAABAAAAARAAAAEgAAABMAAAAUAAAAFQAAABYAAAAXAAAAGAAAABkAAA` +
`AaAAAAGwAAAAQAAAAAAAAALAQAAAUAAAAAAAAANAQAAAYAAAAAAAAAQAQAAAcAAAAAAAAA` +
`SAQAAAgAAAACAAAAAAAAAAgAAAADAAAAAAAAAAsAAAAEAAAAVAQAAAgAAAAFAAAAAAAAAA` +
`kAAAAJAAAAXAQAAAgAAAAKAAAAAAAAAAgAAAAMAAAAAAAAAAoAAAAMAAAAZAQAABsAAAAQ` +
`AAAAAAAAABwAAAAQAAAAbAQAABwAAAAQAAAAZAQAAAQABgAvAAAADwAPACoAAAABAAwAAA` +
`AAAAEADQAwAAAAAwAEACQAAAAFAAYAIgAAAAYACwAoAAAABwACAB4AAAAHAAMAHgAAAAkA` +
`AAAgAAAACQAIACsAAAAKAAoAIQAAAA0ADgAsAAAADwAMAAAAAAAPAAkAIwAAAA8ABQAlAA` +
`AADwAHACYAAAAPAAEAJwAAAA8ACgApAAAADwAMACsAAAAPAA0AMAAAAA8AAAABAAAAAQAA` +
`AAAAAAACAAAAAAAAANIHAAAAAAAAAQABAAEAAACeBwAABgAAAHAQAAAAAGkAAQAOAAcAAw` +
`ADAAEApQcAABkAAAAS8HEQCAAEAAwBbjAHAFEGCgE5AQMADwABECj+DQEaAgEAGgMfAHEw` +
`BgAyASj1DQEo8wAAAQAAAAcAAQABAggXCw4AAAQAAQADAAEAtQcAADMAAABuEA4AAwAMAG` +
`4QDQADAAwBbhACAAEADAETAoAAbjADABACDABUAQAAOQEKABoAAQAaAS4AcSAFABAADgBU` +
`AAAAGgEdAG4gBAAQAAwAcRAKAAAAKPQNABoBAQAaAi0AcTAGACEAKOsAAAAAAAApAAEAAQ` +
`ELKgIAAQABAAAAxQcAAAkAAABuEAwAAQAMAG4QCQAAAAwAEQAAAAIAAgACAAAAygcAAAcA` +
`AABwEBEAAABvIAEAEAAOAAAAAgAAAAAAAAADAAAAAAAAAAAAAAACAAAADAAMAAMAAAAMAA` +
`wADgAAAAIAAAACAAAAAQAAAAAAAAABAAAADAAAAAEAAAAGAAY8aW5pdD4AAkdvABVHb05h` +
`dGl2ZUFjdGl2aXR5LmphdmEAAUkAA0lJSQAESUlJSQADSUxMAARJTExMAAFMAAJMSQACTE` +
`wAA0xMSQAcTGFuZHJvaWQvYXBwL05hdGl2ZUFjdGl2aXR5OwAfTGFuZHJvaWQvY29udGVu` +
`dC9Db21wb25lbnROYW1lOwAYTGFuZHJvaWQvY29udGVudC9JbnRlbnQ7ACFMYW5kcm9pZC` +
`9jb250ZW50L3BtL0FjdGl2aXR5SW5mbzsAI0xhbmRyb2lkL2NvbnRlbnQvcG0vUGFja2Fn` +
`ZU1hbmFnZXI7ABNMYW5kcm9pZC9vcy9CdW5kbGU7ABJMYW5kcm9pZC91dGlsL0xvZzsAM0` +
`xhbmRyb2lkL3ZpZXcvS2V5Q2hhcmFjdGVyTWFwJFVuYXZhaWxhYmxlRXhjZXB0aW9uOwAe` +
`TGFuZHJvaWQvdmlldy9LZXlDaGFyYWN0ZXJNYXA7AA5MamF2YS9pby9GaWxlOwAVTGphdm` +
`EvbGFuZy9FeGNlcHRpb247ABJMamF2YS9sYW5nL1N0cmluZzsAEkxqYXZhL2xhbmcvU3lz` +
`dGVtOwAVTGphdmEvbGFuZy9UaHJvd2FibGU7ACFMb3JnL2dvbGFuZy9hcHAvR29OYXRpdm` +
`VBY3Rpdml0eTsAAVYAAlZMABRhbmRyb2lkLmFwcC5saWJfbmFtZQABZQAhZXhjZXB0aW9u` +
`IHJlYWRpbmcgS2V5Q2hhcmFjdGVyTWFwAANnZXQAD2dldEFic29sdXRlUGF0aAAPZ2V0QW` +
`N0aXZpdHlJbmZvAAtnZXRDYWNoZURpcgAMZ2V0Q29tcG9uZW50AAlnZXRJbnRlbnQAEWdl` +
`dFBhY2thZ2VNYW5hZ2VyAAdnZXRSdW5lAAlnZXRTdHJpbmcACWdldFRtcGRpcgAQZ29OYX` +
`RpdmVBY3Rpdml0eQAEbG9hZAALbG9hZExpYnJhcnkAEmxvYWRMaWJyYXJ5IGZhaWxlZAAn` +
`bG9hZExpYnJhcnk6IG5vIG1hbmlmZXN0IG1ldGFkYXRhIGZvdW5kAAhtZXRhRGF0YQAIb2` +
`5DcmVhdGUAEAAHDjwtABoDAAAABx2HNAJ7LCAegwAxAAcOS6NMS38Cex2HSx4AFQAHDgBA` +
`AQAHDjw8AAEAAwIBCguBgAT0BQQIkAYCAuQGEADoBwIBjAgAAA0AAAAAAAAAAQAAAAAAAA` +
`ABAAAAMQAAAHAAAAACAAAAEQAAADQBAAADAAAADwAAAHgBAAAEAAAAAgAAACwCAAAFAAAA` +
`EwAAADwCAAAGAAAAAQAAANQCAAABIAAABQAAAPQCAAABEAAACAAAACwEAAACIAAAMQAAAH` +
`IEAAADIAAABQAAAJ4HAAAAIAAAAQAAANIHAAAAEAAAAQAAAPAHAAA=` +
``

170
cmd/gomobile/doc.go Normal file
View file

@ -0,0 +1,170 @@
// 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.
// Code generated by 'gomobile help documentation doc.go'. DO NOT EDIT.
/*
Gomobile is a tool for building and running mobile apps written in Go.
To install:
$ go install golang.org/x/mobile/cmd/gomobile@latest
$ gomobile init
At least Go 1.16 is required.
For detailed instructions, see https://golang.org/wiki/Mobile.
Usage:
gomobile command [arguments]
Commands:
bind build a library for Android and iOS
build compile android APK and iOS app
clean remove object files and cached gomobile files
init build OpenAL for Android
install compile android APK and install on device
version print version
Use 'gomobile help [command]' for more information about that command.
# Build a library for Android and iOS
Usage:
gomobile bind [-target android|ios|iossimulator|macos|maccatalyst] [-bootclasspath <path>] [-classpath <path>] [-o output] [build flags] [package]
Bind generates language bindings for the package named by the import
path, and compiles a library for the named target system.
The -target flag takes either android (the default), or one or more
comma-delimited Apple platforms (ios, iossimulator, macos, maccatalyst).
For -target android, the bind command produces an AAR (Android ARchive)
file that archives the precompiled Java API stub classes, the compiled
shared libraries, and all asset files in the /assets subdirectory under
the package directory. The output is named '<package_name>.aar' by
default. This AAR file is commonly used for binary distribution of an
Android library project and most Android IDEs support AAR import. For
example, in Android Studio (1.2+), an AAR file can be imported using
the module import wizard (File > New > New Module > Import .JAR or
.AAR package), and setting it as a new dependency
(File > Project Structure > Dependencies). This requires 'javac'
(version 1.7+) and Android SDK (API level 16 or newer) to build the
library for Android. The environment variable ANDROID_HOME must be set
to the path to Android SDK. Use the -javapkg flag to specify the Java
package prefix for the generated classes.
By default, -target=android builds shared libraries for all supported
instruction sets (arm, arm64, 386, amd64). 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. The generated Objective-C types can be prefixed with the
-prefix flag.
For -target android, the -bootclasspath and -classpath flags are used to
control the bootstrap classpath and the classpath for Go wrappers to Java
classes.
The -v flag provides verbose output, including the list of packages built.
The build flags -a, -n, -x, -gcflags, -ldflags, -tags, -trimpath, and -work
are shared with the build command. For documentation, see 'go help build'.
# Compile android APK and iOS app
Usage:
gomobile build [-target android|ios|iossimulator|macos|maccatalyst] [-o output] [-bundleid bundleID] [build flags] [package]
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 (ios, iossimulator, macos, maccatalyst).
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.
Flag -iosversion sets the minimal version of the iOS SDK to compile against.
The default version is 13.0.
Flag -androidapi sets the Android API version to compile against.
The default and minimum is 16.
The -bundleid flag is required for -target ios and sets the bundle ID to use
with the app.
The -o flag specifies the output file name. If not specified, the
output file name depends on the package built.
The -v flag provides verbose output, including the list of packages built.
The build flags -a, -i, -n, -x, -gcflags, -ldflags, -tags, -trimpath, and -work are
shared with the build command. For documentation, see 'go help build'.
# Remove object files and cached gomobile files
Usage:
gomobile clean
Clean removes object files and cached NDK files downloaded by gomobile init.
# Build OpenAL for Android
Usage:
gomobile init [-openal dir]
If a OpenAL source directory is specified with -openal, init will
build an Android version of OpenAL for use with gomobile build
and gomobile install.
# Compile android APK and install on device
Usage:
gomobile install [-target android] [build flags] [package]
Install compiles and installs the app named by the import path on the
attached mobile device.
Only -target android is supported. The 'adb' tool must be on the PATH.
The build flags -a, -i, -n, -x, -gcflags, -ldflags, -tags, -trimpath, and -work are
shared with the build command.
For documentation, see 'go help build'.
# Print version
Usage:
gomobile version
Version prints versions of the gomobile binary and tools
*/
package main

626
cmd/gomobile/env.go Normal file
View file

@ -0,0 +1,626 @@
// 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.
package main
import (
"bufio"
"encoding/json"
"errors"
"fmt"
"io/fs"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"golang.org/x/mobile/internal/sdkpath"
)
// General mobile build environment. Initialized by envInit.
var (
gomobilepath string // $GOPATH/pkg/gomobile
androidEnv map[string][]string // android arch -> []string
appleEnv map[string][]string
appleNM string
)
func isAndroidPlatform(platform string) bool {
return platform == "android"
}
func isApplePlatform(platform string) bool {
return contains(applePlatforms, platform)
}
var applePlatforms = []string{"ios", "iossimulator", "macos", "maccatalyst"}
func platformArchs(platform string) []string {
switch platform {
case "ios":
return []string{"arm64"}
case "iossimulator":
return []string{"arm64", "amd64"}
case "macos", "maccatalyst":
return []string{"arm64", "amd64"}
case "android":
return []string{"arm", "arm64", "386", "amd64"}
default:
panic(fmt.Sprintf("unexpected platform: %s", platform))
}
}
func isSupportedArch(platform, arch string) bool {
return contains(platformArchs(platform), arch)
}
// platformOS returns the correct GOOS value for platform.
func platformOS(platform string) string {
switch platform {
case "android":
return "android"
case "ios", "iossimulator":
return "ios"
case "macos", "maccatalyst":
// For "maccatalyst", Go packages should be built with GOOS=darwin,
// not GOOS=ios, since the underlying OS (and kernel, runtime) is macOS.
// But, using GOOS=darwin with build-tag ios leads to corrupt builds: https://go.dev/issue/52299
// => So we use GOOS=ios for now.
// We also apply a "macos" or "maccatalyst" build tag, respectively.
// See below for additional context.
return "ios"
default:
panic(fmt.Sprintf("unexpected platform: %s", platform))
}
}
func platformTags(platform string) []string {
switch platform {
case "android":
return []string{"android"}
case "ios", "iossimulator":
return []string{"ios"}
case "macos":
return []string{"macos"}
case "maccatalyst":
// Mac Catalyst is a subset of iOS APIs made available on macOS
// designed to ease porting apps developed for iPad to macOS.
// See
// https://developer.apple.com/mac-catalyst/.
// https://stackoverflow.com/questions/12132933/preprocessor-macro-for-os-x-targets/49560690#49560690
//
// Historically gomobile used GOOS=darwin with build tag ios when
// targeting Mac Catalyst. However, this configuration is not officially
// supported and leads to corrupt builds after go1.18: https://go.dev/issues/52299
// Use GOOS=ios.
// To help discriminate between darwin, ios, macos, and maccatalyst
// targets, there is also a "maccatalyst" tag.
return []string{"macos", "maccatalyst"}
default:
panic(fmt.Sprintf("unexpected platform: %s", platform))
}
}
func contains(haystack []string, needle string) bool {
for _, v := range haystack {
if v == needle {
return true
}
}
return false
}
func buildEnvInit() (cleanup func(), err error) {
// Find gomobilepath.
gopath := goEnv("GOPATH")
for _, p := range filepath.SplitList(gopath) {
gomobilepath = filepath.Join(p, "pkg", "gomobile")
if _, err := os.Stat(gomobilepath); buildN || err == nil {
break
}
}
if buildX {
fmt.Fprintln(xout, "GOMOBILE="+gomobilepath)
}
// Check the toolchain is in a good state.
// Pick a temporary directory for assembling an apk/app.
if gomobilepath == "" {
return nil, errors.New("toolchain not installed, run `gomobile init`")
}
cleanupFn := func() {
if buildWork {
fmt.Printf("WORK=%s\n", tmpdir)
return
}
removeAll(tmpdir)
}
if buildN {
tmpdir = "$WORK"
cleanupFn = func() {}
} else {
tmpdir, err = os.MkdirTemp("", "gomobile-work-")
if err != nil {
return nil, err
}
}
if buildX {
fmt.Fprintln(xout, "WORK="+tmpdir)
}
if err := envInit(); err != nil {
return nil, err
}
return cleanupFn, nil
}
func envInit() (err error) {
// Setup the cross-compiler environments.
if ndkRoot, err := ndkRoot(); err == nil {
androidEnv = make(map[string][]string)
if buildAndroidAPI < minAndroidAPI {
return fmt.Errorf("gomobile requires Android API level >= %d", minAndroidAPI)
}
for arch, toolchain := range ndk {
clang := toolchain.Path(ndkRoot, "clang")
clangpp := toolchain.Path(ndkRoot, "clang++")
if !buildN {
tools := []string{clang, clangpp}
if runtime.GOOS == "windows" {
// Because of https://github.com/android-ndk/ndk/issues/920,
// we require r19c, not just r19b. Fortunately, the clang++.cmd
// script only exists in r19c.
tools = append(tools, clangpp+".cmd")
}
for _, tool := range tools {
_, err = os.Stat(tool)
if err != nil {
return fmt.Errorf("No compiler for %s was found in the NDK (tried %s). Make sure your NDK version is >= r19c. Use `sdkmanager --update` to update it.", arch, tool)
}
}
}
androidEnv[arch] = []string{
"GOOS=android",
"GOARCH=" + arch,
"CC=" + clang,
"CXX=" + clangpp,
"CGO_ENABLED=1",
}
if arch == "arm" {
androidEnv[arch] = append(androidEnv[arch], "GOARM=7")
}
}
}
if !xcodeAvailable() {
return nil
}
appleNM = "nm"
appleEnv = make(map[string][]string)
for _, platform := range applePlatforms {
for _, arch := range platformArchs(platform) {
var env []string
var goos, sdk, clang, cflags string
var err error
switch platform {
case "ios":
goos = "ios"
sdk = "iphoneos"
clang, cflags, err = envClang(sdk)
cflags += " -miphoneos-version-min=" + buildIOSVersion
cflags += " -fembed-bitcode"
case "iossimulator":
goos = "ios"
sdk = "iphonesimulator"
clang, cflags, err = envClang(sdk)
cflags += " -mios-simulator-version-min=" + buildIOSVersion
cflags += " -fembed-bitcode"
case "maccatalyst":
// See the comment about maccatalyst's GOOS, build tags configuration
// in platformOS and platformTags.
// Using GOOS=darwin with build-tag ios leads to corrupt builds: https://go.dev/issue/52299
// => So we use GOOS=ios for now.
goos = "ios"
sdk = "macosx"
clang, cflags, err = envClang(sdk)
// TODO(ydnar): the following 3 lines MAY be needed to compile
// packages or apps for maccatalyst. Commenting them out now in case
// it turns out they are necessary. Currently none of the example
// apps will build for macos or maccatalyst because they have a
// GLKit dependency, which is deprecated on all Apple platforms, and
// broken on maccatalyst (GLKView isnt available).
// sysroot := strings.SplitN(cflags, " ", 2)[1]
// cflags += " -isystem " + sysroot + "/System/iOSSupport/usr/include"
// cflags += " -iframework " + sysroot + "/System/iOSSupport/System/Library/Frameworks"
switch arch {
case "amd64":
cflags += " -target x86_64-apple-ios" + buildIOSVersion + "-macabi"
case "arm64":
cflags += " -target arm64-apple-ios" + buildIOSVersion + "-macabi"
cflags += " -fembed-bitcode"
}
case "macos":
goos = "darwin"
sdk = "macosx" // Note: the SDK is called "macosx", not "macos"
clang, cflags, err = envClang(sdk)
if arch == "arm64" {
cflags += " -fembed-bitcode"
}
default:
panic(fmt.Errorf("unknown Apple target: %s/%s", platform, arch))
}
if err != nil {
return err
}
env = append(env,
"GOOS="+goos,
"GOARCH="+arch,
"GOFLAGS="+"-tags="+strings.Join(platformTags(platform), ","),
"CC="+clang,
"CXX="+clang+"++",
"CGO_CFLAGS="+cflags+" -arch "+archClang(arch),
"CGO_CXXFLAGS="+cflags+" -arch "+archClang(arch),
"CGO_LDFLAGS="+cflags+" -arch "+archClang(arch),
"CGO_ENABLED=1",
"DARWIN_SDK="+sdk,
)
appleEnv[platform+"/"+arch] = env
}
}
return nil
}
// abi maps GOARCH values to Android ABI strings.
// See https://developer.android.com/ndk/guides/abis
func abi(goarch string) string {
switch goarch {
case "arm":
return "armeabi-v7a"
case "arm64":
return "arm64-v8a"
case "386":
return "x86"
case "amd64":
return "x86_64"
default:
return ""
}
}
// checkNDKRoot returns nil if the NDK in `ndkRoot` supports the current configured
// API version and all the specified Android targets.
func checkNDKRoot(ndkRoot string, targets []targetInfo) error {
platformsJson, err := os.Open(filepath.Join(ndkRoot, "meta", "platforms.json"))
if err != nil {
return err
}
defer platformsJson.Close()
decoder := json.NewDecoder(platformsJson)
supportedVersions := struct {
Min int
Max int
}{}
if err := decoder.Decode(&supportedVersions); err != nil {
return err
}
if supportedVersions.Min > buildAndroidAPI ||
supportedVersions.Max < buildAndroidAPI {
return fmt.Errorf("unsupported API version %d (not in %d..%d)", buildAndroidAPI, supportedVersions.Min, supportedVersions.Max)
}
abisJson, err := os.Open(filepath.Join(ndkRoot, "meta", "abis.json"))
if err != nil {
return err
}
defer abisJson.Close()
decoder = json.NewDecoder(abisJson)
abis := make(map[string]struct{})
if err := decoder.Decode(&abis); err != nil {
return err
}
for _, target := range targets {
if !isAndroidPlatform(target.platform) {
continue
}
if _, found := abis[abi(target.arch)]; !found {
return fmt.Errorf("ndk does not support %s", target.platform)
}
}
return nil
}
// compatibleNDKRoots searches the side-by-side NDK dirs for compatible SDKs.
func compatibleNDKRoots(ndkForest string, targets []targetInfo) ([]string, error) {
ndkDirs, err := os.ReadDir(ndkForest)
if err != nil {
return nil, err
}
compatibleNDKRoots := []string{}
var lastErr error
for _, dirent := range ndkDirs {
ndkRoot := filepath.Join(ndkForest, dirent.Name())
lastErr = checkNDKRoot(ndkRoot, targets)
if lastErr == nil {
compatibleNDKRoots = append(compatibleNDKRoots, ndkRoot)
}
}
if len(compatibleNDKRoots) > 0 {
return compatibleNDKRoots, nil
}
return nil, lastErr
}
// ndkVersion returns the full version number of an installed copy of the NDK,
// or "" if it cannot be determined.
func ndkVersion(ndkRoot string) string {
properties, err := os.Open(filepath.Join(ndkRoot, "source.properties"))
if err != nil {
return ""
}
defer properties.Close()
// Parse the version number out of the .properties file.
// See https://en.wikipedia.org/wiki/.properties
scanner := bufio.NewScanner(properties)
for scanner.Scan() {
line := scanner.Text()
tokens := strings.SplitN(line, "=", 2)
if len(tokens) != 2 {
continue
}
if strings.TrimSpace(tokens[0]) == "Pkg.Revision" {
return strings.TrimSpace(tokens[1])
}
}
return ""
}
// ndkRoot returns the root path of an installed NDK that supports all the
// specified Android targets. For details of NDK locations, see
// https://github.com/android/ndk-samples/wiki/Configure-NDK-Path
func ndkRoot(targets ...targetInfo) (string, error) {
if buildN {
return "$NDK_PATH", nil
}
// Try the ANDROID_NDK_HOME variable. This approach is deprecated, but it
// has the highest priority because it represents an explicit user choice.
if ndkRoot := os.Getenv("ANDROID_NDK_HOME"); ndkRoot != "" {
if err := checkNDKRoot(ndkRoot, targets); err != nil {
return "", fmt.Errorf("ANDROID_NDK_HOME specifies %s, which is unusable: %w", ndkRoot, err)
}
return ndkRoot, nil
}
androidHome, err := sdkpath.AndroidHome()
if err != nil {
return "", fmt.Errorf("could not locate Android SDK: %w", err)
}
// Use the newest compatible NDK under the side-by-side path arrangement.
ndkForest := filepath.Join(androidHome, "ndk")
ndkRoots, sideBySideErr := compatibleNDKRoots(ndkForest, targets)
if len(ndkRoots) != 0 {
// Choose the latest version that supports the build configuration.
// NDKs whose version cannot be determined will be least preferred.
// In the event of a tie, the later ndkRoot will win.
maxVersion := ""
var selected string
for _, ndkRoot := range ndkRoots {
version := ndkVersion(ndkRoot)
if version >= maxVersion {
maxVersion = version
selected = ndkRoot
}
}
return selected, nil
}
// Try the deprecated NDK location.
ndkRoot := filepath.Join(androidHome, "ndk-bundle")
if legacyErr := checkNDKRoot(ndkRoot, targets); legacyErr != nil {
return "", fmt.Errorf("no usable NDK in %s: %w, %v", androidHome, sideBySideErr, legacyErr)
}
return ndkRoot, nil
}
func envClang(sdkName string) (clang, cflags string, err error) {
if buildN {
return sdkName + "-clang", "-isysroot " + sdkName, nil
}
cmd := exec.Command("xcrun", "--sdk", sdkName, "--find", "clang")
out, err := cmd.Output()
if err != nil {
if ee := (*exec.ExitError)(nil); errors.As(err, &ee) {
out = append(out, ee.Stderr...)
}
return "", "", fmt.Errorf("xcrun --find: %v\n%s", err, out)
}
clang = strings.TrimSpace(string(out))
cmd = exec.Command("xcrun", "--sdk", sdkName, "--show-sdk-path")
out, err = cmd.Output()
if err != nil {
if ee := (*exec.ExitError)(nil); errors.As(err, &ee) {
out = append(out, ee.Stderr...)
}
return "", "", fmt.Errorf("xcrun --show-sdk-path: %v\n%s", err, out)
}
sdk := strings.TrimSpace(string(out))
return clang, "-isysroot " + sdk, nil
}
func archClang(goarch string) string {
switch goarch {
case "arm":
return "armv7"
case "arm64":
return "arm64"
case "386":
return "i386"
case "amd64":
return "x86_64"
default:
panic(fmt.Sprintf("unknown GOARCH: %q", goarch))
}
}
// environ merges os.Environ and the given "key=value" pairs.
// If a key is in both os.Environ and kv, kv takes precedence.
func environ(kv []string) []string {
cur := os.Environ()
new := make([]string, 0, len(cur)+len(kv))
envs := make(map[string]string, len(cur))
for _, ev := range cur {
elem := strings.SplitN(ev, "=", 2)
if len(elem) != 2 || elem[0] == "" {
// pass the env var of unusual form untouched.
// e.g. Windows may have env var names starting with "=".
new = append(new, ev)
continue
}
if goos == "windows" {
elem[0] = strings.ToUpper(elem[0])
}
envs[elem[0]] = elem[1]
}
for _, ev := range kv {
elem := strings.SplitN(ev, "=", 2)
if len(elem) != 2 || elem[0] == "" {
panic(fmt.Sprintf("malformed env var %q from input", ev))
}
if goos == "windows" {
elem[0] = strings.ToUpper(elem[0])
}
envs[elem[0]] = elem[1]
}
for k, v := range envs {
new = append(new, k+"="+v)
}
return new
}
func getenv(env []string, key string) string {
prefix := key + "="
for _, kv := range env {
if strings.HasPrefix(kv, prefix) {
return kv[len(prefix):]
}
}
return ""
}
func archNDK() string {
if runtime.GOOS == "windows" && runtime.GOARCH == "386" {
return "windows"
} else {
var arch string
switch runtime.GOARCH {
case "386":
arch = "x86"
case "amd64":
arch = "x86_64"
case "arm64":
// Android NDK does not contain arm64 toolchains (until and
// including NDK 23), use use x86_64 instead. See:
// https://github.com/android/ndk/issues/1299
if runtime.GOOS == "darwin" {
arch = "x86_64"
break
}
fallthrough
default:
panic("unsupported GOARCH: " + runtime.GOARCH)
}
return runtime.GOOS + "-" + arch
}
}
type ndkToolchain struct {
arch string
abi string
minAPI int
toolPrefix string
clangPrefix string
}
func (tc *ndkToolchain) ClangPrefix() string {
if buildAndroidAPI < tc.minAPI {
return fmt.Sprintf("%s%d", tc.clangPrefix, tc.minAPI)
}
return fmt.Sprintf("%s%d", tc.clangPrefix, buildAndroidAPI)
}
func (tc *ndkToolchain) Path(ndkRoot, toolName string) string {
cmdFromPref := func(pref string) string {
return filepath.Join(ndkRoot, "toolchains", "llvm", "prebuilt", archNDK(), "bin", pref+"-"+toolName)
}
var cmd string
switch toolName {
case "clang", "clang++":
cmd = cmdFromPref(tc.ClangPrefix())
default:
cmd = cmdFromPref(tc.toolPrefix)
// Starting from NDK 23, GNU binutils are fully migrated to LLVM binutils.
// See https://android.googlesource.com/platform/ndk/+/master/docs/Roadmap.md#ndk-r23
if _, err := os.Stat(cmd); errors.Is(err, fs.ErrNotExist) {
cmd = cmdFromPref("llvm")
}
}
return cmd
}
type ndkConfig map[string]ndkToolchain // map: GOOS->androidConfig.
func (nc ndkConfig) Toolchain(arch string) ndkToolchain {
tc, ok := nc[arch]
if !ok {
panic(`unsupported architecture: ` + arch)
}
return tc
}
var ndk = ndkConfig{
"arm": {
arch: "arm",
abi: "armeabi-v7a",
minAPI: 16,
toolPrefix: "arm-linux-androideabi",
clangPrefix: "armv7a-linux-androideabi",
},
"arm64": {
arch: "arm64",
abi: "arm64-v8a",
minAPI: 21,
toolPrefix: "aarch64-linux-android",
clangPrefix: "aarch64-linux-android",
},
"386": {
arch: "x86",
abi: "x86",
minAPI: 16,
toolPrefix: "i686-linux-android",
clangPrefix: "i686-linux-android",
},
"amd64": {
arch: "x86_64",
abi: "x86_64",
minAPI: 21,
toolPrefix: "x86_64-linux-android",
clangPrefix: "x86_64-linux-android",
},
}
func xcodeAvailable() bool {
err := exec.Command("xcrun", "xcodebuild", "-version").Run()
return err == nil
}

160
cmd/gomobile/env_test.go Normal file
View file

@ -0,0 +1,160 @@
// Copyright 2019 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.
package main
import (
"fmt"
"os"
"path/filepath"
"testing"
)
func TestNdkRoot(t *testing.T) {
home, err := os.MkdirTemp("", "gomobile-test-")
if err != nil {
t.Fatal(err)
}
homeorig := os.Getenv("ANDROID_HOME")
os.Unsetenv("ANDROID_HOME")
ndkhomeorig := os.Getenv("ANDROID_NDK_HOME")
os.Unsetenv("ANDROID_NDK_HOME")
defer func() {
os.Setenv("ANDROID_HOME", homeorig)
os.Setenv("ANDROID_NDK_HOME", ndkhomeorig)
os.RemoveAll(home)
}()
makeMockNDK := func(path, version, platforms, abis string) string {
dir := filepath.Join(home, path)
if err := os.Mkdir(dir, 0755); err != nil {
t.Fatalf("couldn't mkdir %q", dir)
}
propertiesPath := filepath.Join(dir, "source.properties")
propertiesData := []byte("Pkg.Revision = " + version)
if err := os.WriteFile(propertiesPath, propertiesData, 0644); err != nil {
t.Fatalf("couldn't write source.properties: %v", err)
}
metaDir := filepath.Join(dir, "meta")
if err := os.Mkdir(metaDir, 0755); err != nil {
t.Fatalf("couldn't mkdir %q", metaDir)
}
platformsPath := filepath.Join(metaDir, "platforms.json")
platformsData := []byte(platforms)
if err := os.WriteFile(platformsPath, platformsData, 0644); err != nil {
t.Fatalf("couldn't write platforms.json: %v", err)
}
abisPath := filepath.Join(metaDir, "abis.json")
abisData := []byte(abis)
if err := os.WriteFile(abisPath, abisData, 0644); err != nil {
t.Fatalf("couldn't populate abis.json: %v", err)
}
return dir
}
t.Run("no NDK in the default location", func(t *testing.T) {
os.Setenv("ANDROID_HOME", home)
defer os.Unsetenv("ANDROID_HOME")
if ndk, err := ndkRoot(); err == nil {
t.Errorf("expected error but got %q", ndk)
}
})
t.Run("NDK location is set but is wrong", func(t *testing.T) {
os.Setenv("ANDROID_NDK_HOME", filepath.Join(home, "no-such-path"))
defer os.Unsetenv("ANDROID_NDK_HOME")
if ndk, err := ndkRoot(); err == nil {
t.Errorf("expected error but got %q", ndk)
}
})
t.Run("Two NDKs installed", func(t *testing.T) {
// Default path for pre-side-by-side NDKs.
sdkNDK := makeMockNDK("ndk-bundle", "fake-version", `{"min":16,"max":32}`, "{}")
defer os.RemoveAll(sdkNDK)
// Arbitrary location for testing ANDROID_NDK_HOME.
envNDK := makeMockNDK("custom-location", "fake-version", `{"min":16,"max":32}`, "{}")
// ANDROID_NDK_HOME is sufficient.
os.Setenv("ANDROID_NDK_HOME", envNDK)
if ndk, err := ndkRoot(); ndk != envNDK {
t.Errorf("got (%q, %v) want (%q, nil)", ndk, err, envNDK)
}
// ANDROID_NDK_HOME takes precedence over ANDROID_HOME.
os.Setenv("ANDROID_HOME", home)
if ndk, err := ndkRoot(); ndk != envNDK {
t.Errorf("got (%q, %v) want (%q, nil)", ndk, err, envNDK)
}
// ANDROID_NDK_HOME is respected even if there is no NDK there.
os.RemoveAll(envNDK)
if ndk, err := ndkRoot(); err == nil {
t.Errorf("expected error but got %q", ndk)
}
// ANDROID_HOME is used if ANDROID_NDK_HOME is not set.
os.Unsetenv("ANDROID_NDK_HOME")
if ndk, err := ndkRoot(); ndk != sdkNDK {
t.Errorf("got (%q, %v) want (%q, nil)", ndk, err, envNDK)
}
})
t.Run("Modern 'side-by-side' NDK selection", func(t *testing.T) {
defer func() {
buildAndroidAPI = minAndroidAPI
}()
ndkForest := filepath.Join(home, "ndk")
if err := os.Mkdir(ndkForest, 0755); err != nil {
t.Fatalf("couldn't mkdir %q", ndkForest)
}
path := filepath.Join("ndk", "newer")
platforms := `{"min":19,"max":32}`
abis := `{"arm64-v8a": {}, "armeabi-v7a": {}, "x86_64": {}}`
version := "17.2.0"
newerNDK := makeMockNDK(path, version, platforms, abis)
path = filepath.Join("ndk", "older")
platforms = `{"min":16,"max":31}`
abis = `{"arm64-v8a": {}, "armeabi-v7a": {}, "x86": {}}`
version = "17.1.0"
olderNDK := makeMockNDK(path, version, platforms, abis)
testCases := []struct {
api int
targets []targetInfo
wantNDKRoot string
}{
{15, nil, ""},
{16, nil, olderNDK},
{16, []targetInfo{{"android", "arm"}}, olderNDK},
{16, []targetInfo{{"android", "arm"}, {"android", "arm64"}}, olderNDK},
{16, []targetInfo{{"android", "x86_64"}}, ""},
{19, nil, newerNDK},
{19, []targetInfo{{"android", "arm"}}, newerNDK},
{19, []targetInfo{{"android", "arm"}, {"android", "arm64"}, {"android", "386"}}, olderNDK},
{32, nil, newerNDK},
{32, []targetInfo{{"android", "arm"}}, newerNDK},
{32, []targetInfo{{"android", "386"}}, ""},
}
for i, tc := range testCases {
t.Run(fmt.Sprintf("%d", i), func(t *testing.T) {
buildAndroidAPI = tc.api
ndk, err := ndkRoot(tc.targets...)
if len(tc.wantNDKRoot) != 0 {
if ndk != tc.wantNDKRoot || err != nil {
t.Errorf("got (%q, %v), want (%q, nil)", ndk, err, tc.wantNDKRoot)
}
} else if err == nil {
t.Error("expected error")
}
})
}
})
}

158
cmd/gomobile/gendex.go Normal file
View file

@ -0,0 +1,158 @@
// 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:build ignore
// Gendex generates a dex file used by Go apps created with gomobile.
//
// The dex is a thin extension of NativeActivity, providing access to
// a few platform features (not the SDK UI) not easily accessible from
// NDK headers. Long term these could be made part of the standard NDK,
// however that would limit gomobile to working with newer versions of
// the Android OS, so we do this while we wait.
//
// Respects ANDROID_HOME to set the path of the Android SDK.
// javac must be on the PATH.
package main
import (
"bytes"
"encoding/base64"
"errors"
"flag"
"fmt"
"go/format"
"log"
"os"
"os/exec"
"path/filepath"
"golang.org/x/mobile/internal/sdkpath"
)
var outfile = flag.String("o", "", "result will be written file")
var tmpdir string
func main() {
flag.Parse()
var err error
tmpdir, err = os.MkdirTemp("", "gendex-")
if err != nil {
log.Fatal(err)
}
err = gendex()
os.RemoveAll(tmpdir)
if err != nil {
log.Fatal(err)
}
}
func gendex() error {
androidHome, err := sdkpath.AndroidHome()
if err != nil {
return fmt.Errorf("couldn't find Android SDK: %w", err)
}
if err := os.MkdirAll(tmpdir+"/work/org/golang/app", 0775); err != nil {
return err
}
javaFiles, err := filepath.Glob("../../app/*.java")
if err != nil {
return err
}
if len(javaFiles) == 0 {
return errors.New("could not find ../../app/*.java files")
}
platform, err := findLast(androidHome + "/platforms")
if err != nil {
return err
}
cmd := exec.Command(
"javac",
"-source", "1.8",
"-target", "1.8",
"-bootclasspath", platform+"/android.jar",
"-d", tmpdir+"/work",
)
cmd.Args = append(cmd.Args, javaFiles...)
if out, err := cmd.CombinedOutput(); err != nil {
fmt.Println(cmd.Args)
os.Stderr.Write(out)
return err
}
buildTools, err := findLast(androidHome + "/build-tools")
if err != nil {
return err
}
cmd = exec.Command(
buildTools+"/dx",
"--dex",
"--output="+tmpdir+"/classes.dex",
tmpdir+"/work",
)
if out, err := cmd.CombinedOutput(); err != nil {
os.Stderr.Write(out)
return err
}
src, err := os.ReadFile(tmpdir + "/classes.dex")
if err != nil {
return err
}
data := base64.StdEncoding.EncodeToString(src)
buf := new(bytes.Buffer)
fmt.Fprint(buf, header)
var piece string
for len(data) > 0 {
l := 70
if l > len(data) {
l = len(data)
}
piece, data = data[:l], data[l:]
fmt.Fprintf(buf, "\t`%s` + \n", piece)
}
fmt.Fprintf(buf, "\t``")
out, err := format.Source(buf.Bytes())
if err != nil {
buf.WriteTo(os.Stderr)
return err
}
w, err := os.Create(*outfile)
if err != nil {
return err
}
if _, err := w.Write(out); err != nil {
return err
}
if err := w.Close(); err != nil {
return err
}
return nil
}
func findLast(path string) (string, error) {
dir, err := os.Open(path)
if err != nil {
return "", err
}
children, err := dir.Readdirnames(-1)
if err != nil {
return "", err
}
return path + "/" + children[len(children)-1], nil
}
var header = `// 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.
// Code generated by gendex.go. DO NOT EDIT.
package main
var dexStr = `

354
cmd/gomobile/init.go Normal file
View file

@ -0,0 +1,354 @@
// 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.
package main
import (
"bytes"
"errors"
"fmt"
"io"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"time"
"golang.org/x/mobile/internal/sdkpath"
)
var (
goos = runtime.GOOS
goarch = runtime.GOARCH
)
var cmdInit = &command{
run: runInit,
Name: "init",
Usage: "[-openal dir]",
Short: "build OpenAL for Android",
Long: `
If a OpenAL source directory is specified with -openal, init will
build an Android version of OpenAL for use with gomobile build
and gomobile install.
`,
}
var initOpenAL string // -openal
func init() {
cmdInit.flag.StringVar(&initOpenAL, "openal", "", "OpenAL source path")
}
func runInit(cmd *command) error {
gopaths := filepath.SplitList(goEnv("GOPATH"))
if len(gopaths) == 0 {
return fmt.Errorf("GOPATH is not set")
}
gomobilepath = filepath.Join(gopaths[0], "pkg/gomobile")
if buildX || buildN {
fmt.Fprintln(xout, "GOMOBILE="+gomobilepath)
}
removeAll(gomobilepath)
if err := mkdir(gomobilepath); err != nil {
return err
}
if buildN {
tmpdir = filepath.Join(gomobilepath, "work")
} else {
var err error
tmpdir, err = os.MkdirTemp(gomobilepath, "work-")
if err != nil {
return err
}
}
if buildX || buildN {
fmt.Fprintln(xout, "WORK="+tmpdir)
}
defer func() {
if buildWork {
fmt.Printf("WORK=%s\n", tmpdir)
return
}
removeAll(tmpdir)
}()
// Make sure gobind is up to date.
if err := goInstall([]string{"golang.org/x/mobile/cmd/gobind@latest"}, nil); err != nil {
return err
}
if buildN {
initOpenAL = "$OPENAL_PATH"
} else {
if initOpenAL != "" {
var err error
if initOpenAL, err = filepath.Abs(initOpenAL); err != nil {
return err
}
}
}
if err := envInit(); err != nil {
return err
}
start := time.Now()
if err := installOpenAL(gomobilepath); err != nil {
return err
}
if buildV {
took := time.Since(start) / time.Second * time.Second
fmt.Fprintf(os.Stderr, "\nDone, build took %s.\n", took)
}
return nil
}
func installOpenAL(gomobilepath string) error {
if initOpenAL == "" {
return nil
}
ndkRoot, err := ndkRoot()
if err != nil {
return err
}
var cmake string
if buildN {
cmake = "cmake"
} else {
sdkRoot, err := sdkpath.AndroidHome()
if err != nil {
return nil
}
cmake, err = exec.LookPath("cmake")
if err != nil {
cmakePath := filepath.Join(sdkRoot, "cmake")
cmakeDir, err := os.Open(cmakePath)
if err != nil {
if os.IsNotExist(err) {
// Skip OpenAL install if the cmake package is not installed.
return errors.New("cmake was not found in the PATH. Please install it through the Android SDK manager.")
}
return err
}
defer cmakeDir.Close()
// There might be multiple versions of CMake installed. Use any one for now.
cmakeVers, err := cmakeDir.Readdirnames(1)
if err != nil || len(cmakeVers) == 0 {
return errors.New("cmake was not found in the PATH. Please install it through the Android SDK manager.")
}
cmake = filepath.Join(cmakePath, cmakeVers[0], "bin", "cmake")
}
}
var alTmpDir string
if buildN {
alTmpDir = filepath.Join(gomobilepath, "work")
} else {
var err error
alTmpDir, err = os.MkdirTemp(gomobilepath, "openal-release-")
if err != nil {
return err
}
defer removeAll(alTmpDir)
}
for _, f := range []string{"include/AL/al.h", "include/AL/alc.h"} {
dst := filepath.Join(gomobilepath, f)
src := filepath.Join(initOpenAL, f)
if err := copyFile(dst, src); err != nil {
return err
}
}
for _, arch := range platformArchs("android") {
t := ndk[arch]
abi := t.arch
if abi == "arm" {
abi = "armeabi"
}
make := filepath.Join(ndkRoot, "prebuilt", archNDK(), "bin", "make")
// Split android-XX to get the api version.
buildDir := alTmpDir + "/build/" + abi
if err := mkdir(buildDir); err != nil {
return err
}
cmd := exec.Command(cmake,
initOpenAL,
"-DCMAKE_TOOLCHAIN_FILE="+initOpenAL+"/XCompile-Android.txt",
"-DHOST="+t.ClangPrefix())
cmd.Dir = buildDir
tcPath := filepath.Join(ndkRoot, "toolchains", "llvm", "prebuilt", archNDK(), "bin")
if !buildN {
orgPath := os.Getenv("PATH")
cmd.Env = []string{"PATH=" + tcPath + string(os.PathListSeparator) + orgPath}
}
if err := runCmd(cmd); err != nil {
return err
}
cmd = exec.Command(make)
cmd.Dir = buildDir
if err := runCmd(cmd); err != nil {
return err
}
dst := filepath.Join(gomobilepath, "lib", t.abi, "libopenal.so")
src := filepath.Join(alTmpDir, "build", abi, "libopenal.so")
if err := copyFile(dst, src); err != nil {
return err
}
}
return nil
}
func mkdir(dir string) error {
if buildX || buildN {
printcmd("mkdir -p %s", dir)
}
if buildN {
return nil
}
return os.MkdirAll(dir, 0755)
}
func symlink(src, dst string) error {
if buildX || buildN {
printcmd("ln -s %s %s", src, dst)
}
if buildN {
return nil
}
if goos == "windows" {
return doCopyAll(dst, src)
}
return os.Symlink(src, dst)
}
func doCopyAll(dst, src string) error {
return filepath.Walk(src, func(path string, info os.FileInfo, errin error) (err error) {
if errin != nil {
return errin
}
prefixLen := len(src)
if len(path) > prefixLen {
prefixLen++ // file separator
}
outpath := filepath.Join(dst, path[prefixLen:])
if info.IsDir() {
return os.Mkdir(outpath, 0755)
}
in, err := os.Open(path)
if err != nil {
return err
}
defer in.Close()
out, err := os.OpenFile(outpath, os.O_CREATE|os.O_EXCL|os.O_WRONLY, info.Mode())
if err != nil {
return err
}
defer func() {
if errc := out.Close(); err == nil {
err = errc
}
}()
_, err = io.Copy(out, in)
return err
})
}
func removeAll(path string) error {
if buildX || buildN {
printcmd(`rm -r -f "%s"`, path)
}
if buildN {
return nil
}
// os.RemoveAll behaves differently in windows.
// http://golang.org/issues/9606
if goos == "windows" {
resetReadOnlyFlagAll(path)
}
return os.RemoveAll(path)
}
func resetReadOnlyFlagAll(path string) error {
fi, err := os.Stat(path)
if err != nil {
return err
}
if !fi.IsDir() {
return os.Chmod(path, 0666)
}
fd, err := os.Open(path)
if err != nil {
return err
}
defer fd.Close()
names, _ := fd.Readdirnames(-1)
for _, name := range names {
resetReadOnlyFlagAll(path + string(filepath.Separator) + name)
}
return nil
}
func goEnv(name string) string {
if val := os.Getenv(name); val != "" {
return val
}
val, err := exec.Command("go", "env", name).Output()
if err != nil {
panic(err) // the Go tool was tested to work earlier
}
return strings.TrimSpace(string(val))
}
func runCmd(cmd *exec.Cmd) error {
if buildX || buildN {
dir := ""
if cmd.Dir != "" {
dir = "PWD=" + cmd.Dir + " "
}
env := strings.Join(cmd.Env, " ")
if env != "" {
env += " "
}
printcmd("%s%s%s", dir, env, strings.Join(cmd.Args, " "))
}
buf := new(bytes.Buffer)
buf.WriteByte('\n')
if buildV {
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
} else {
cmd.Stdout = buf
cmd.Stderr = buf
}
if buildWork {
if goos == "windows" {
cmd.Env = append(cmd.Env, `TEMP=`+tmpdir)
cmd.Env = append(cmd.Env, `TMP=`+tmpdir)
} else {
cmd.Env = append(cmd.Env, `TMPDIR=`+tmpdir)
}
}
if !buildN {
cmd.Env = environ(cmd.Env)
if err := cmd.Run(); err != nil {
return fmt.Errorf("%s failed: %v%s", strings.Join(cmd.Args, " "), err, buf)
}
}
return nil
}

203
cmd/gomobile/init_test.go Normal file
View file

@ -0,0 +1,203 @@
// 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.
package main
import (
"bytes"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"testing"
"text/template"
)
var gopath string
func TestInit(t *testing.T) {
if runtime.GOOS == "android" || runtime.GOOS == "ios" {
t.Skipf("not available on %s", runtime.GOOS)
}
if _, err := exec.LookPath("diff"); err != nil {
t.Skip("command diff not found, skipping")
}
buf := new(bytes.Buffer)
gopathorig := os.Getenv("GOPATH")
defer func() {
xout = os.Stderr
buildN = false
buildX = false
os.Setenv("GOPATH", gopathorig)
}()
xout = buf
buildN = true
buildX = true
// Test that first GOPATH element is chosen correctly.
var err error
gopath, err = os.MkdirTemp("", "gomobile-test")
if err != nil {
t.Fatal(err)
}
paths := []string{gopath, "/path2", "/path3"}
if goos == "windows" {
gopath = filepath.ToSlash(`C:\GOPATH1`)
paths = []string{gopath, `C:\PATH2`, `C:\PATH3`}
}
os.Setenv("GOPATH", strings.Join(paths, string(os.PathListSeparator)))
os.Setenv("GOROOT_BOOTSTRAP", "go1.4")
if goos == "windows" {
os.Setenv("HOMEDRIVE", "C:")
}
emptymod, err := os.MkdirTemp("", "gomobile-test")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(emptymod)
// Create go.mod, but without Go files.
f, err := os.Create(filepath.Join(emptymod, "go.mod"))
if err != nil {
t.Fatal(err)
}
defer f.Close()
if _, err := f.WriteString("module example.com/m\n"); err != nil {
t.Fatal(err)
}
if err := f.Sync(); err != nil {
t.Fatal(err)
}
dirs := []struct {
dir string
name string
}{
{
dir: ".",
name: "current",
},
{
dir: emptymod,
name: "emptymod",
},
}
for _, dir := range dirs {
dir := dir
t.Run(dir.name, func(t *testing.T) {
wd, err := os.Getwd()
if err != nil {
t.Fatal(err)
}
if err := os.Chdir(dir.dir); err != nil {
t.Fatal(err)
}
defer os.Chdir(wd)
if err := runInit(cmdInit); err != nil {
t.Log(buf.String())
t.Fatal(err)
}
if dir.name == "emptymod" {
return
}
diff, err := diffOutput(buf.String(), initTmpl)
if err != nil {
t.Fatalf("computing diff failed: %v", err)
}
if diff != "" {
t.Errorf("unexpected output:\n%s", diff)
}
})
}
}
func diffOutput(got string, wantTmpl *template.Template) (string, error) {
got = filepath.ToSlash(got)
wantBuf := new(bytes.Buffer)
data, err := defaultOutputData("")
if err != nil {
return "", err
}
if err := wantTmpl.Execute(wantBuf, data); err != nil {
return "", err
}
want := wantBuf.String()
if got != want {
return diff(got, want)
}
return "", nil
}
type outputData struct {
GOOS string
GOARCH string
GOPATH string
NDKARCH string
EXE string // .extension for executables. (ex. ".exe" for windows)
Xproj string
Xcontents string
Xinfo infoplistTmplData
}
func defaultOutputData(teamID string) (outputData, error) {
projPbxproj := new(bytes.Buffer)
if err := projPbxprojTmpl.Execute(projPbxproj, projPbxprojTmplData{
TeamID: teamID,
}); err != nil {
return outputData{}, err
}
data := outputData{
GOOS: goos,
GOARCH: goarch,
GOPATH: gopath,
NDKARCH: archNDK(),
Xproj: projPbxproj.String(),
Xcontents: contentsJSON,
Xinfo: infoplistTmplData{BundleID: "org.golang.todo.basic", Name: "Basic"},
}
if goos == "windows" {
data.EXE = ".exe"
}
return data, nil
}
var initTmpl = template.Must(template.New("output").Parse(`GOMOBILE={{.GOPATH}}/pkg/gomobile
rm -r -f "$GOMOBILE"
mkdir -p $GOMOBILE
WORK={{.GOPATH}}/pkg/gomobile/work
GOMODCACHE={{.GOPATH}}/pkg/mod go install -x golang.org/x/mobile/cmd/gobind@latest
cp $OPENAL_PATH/include/AL/al.h $GOMOBILE/include/AL/al.h
mkdir -p $GOMOBILE/include/AL
cp $OPENAL_PATH/include/AL/alc.h $GOMOBILE/include/AL/alc.h
mkdir -p $GOMOBILE/include/AL
mkdir -p $WORK/build/armeabi
PWD=$WORK/build/armeabi cmake $OPENAL_PATH -DCMAKE_TOOLCHAIN_FILE=$OPENAL_PATH/XCompile-Android.txt -DHOST=armv7a-linux-androideabi16
PWD=$WORK/build/armeabi $NDK_PATH/prebuilt/{{.NDKARCH}}/bin/make
cp $WORK/build/armeabi/libopenal.so $GOMOBILE/lib/armeabi-v7a/libopenal.so
mkdir -p $GOMOBILE/lib/armeabi-v7a
mkdir -p $WORK/build/arm64
PWD=$WORK/build/arm64 cmake $OPENAL_PATH -DCMAKE_TOOLCHAIN_FILE=$OPENAL_PATH/XCompile-Android.txt -DHOST=aarch64-linux-android21
PWD=$WORK/build/arm64 $NDK_PATH/prebuilt/{{.NDKARCH}}/bin/make
cp $WORK/build/arm64/libopenal.so $GOMOBILE/lib/arm64-v8a/libopenal.so
mkdir -p $GOMOBILE/lib/arm64-v8a
mkdir -p $WORK/build/x86
PWD=$WORK/build/x86 cmake $OPENAL_PATH -DCMAKE_TOOLCHAIN_FILE=$OPENAL_PATH/XCompile-Android.txt -DHOST=i686-linux-android16
PWD=$WORK/build/x86 $NDK_PATH/prebuilt/{{.NDKARCH}}/bin/make
cp $WORK/build/x86/libopenal.so $GOMOBILE/lib/x86/libopenal.so
mkdir -p $GOMOBILE/lib/x86
mkdir -p $WORK/build/x86_64
PWD=$WORK/build/x86_64 cmake $OPENAL_PATH -DCMAKE_TOOLCHAIN_FILE=$OPENAL_PATH/XCompile-Android.txt -DHOST=x86_64-linux-android21
PWD=$WORK/build/x86_64 $NDK_PATH/prebuilt/{{.NDKARCH}}/bin/make
cp $WORK/build/x86_64/libopenal.so $GOMOBILE/lib/x86_64/libopenal.so
mkdir -p $GOMOBILE/lib/x86_64
rm -r -f "$WORK"
`))

57
cmd/gomobile/install.go Normal file
View file

@ -0,0 +1,57 @@
// 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.
package main
import (
"fmt"
"os"
"os/exec"
"path"
"strings"
)
var cmdInstall = &command{
run: runInstall,
Name: "install",
Usage: "[-target android] [build flags] [package]",
Short: "compile android APK and install on device",
Long: `
Install compiles and installs the app named by the import path on the
attached mobile device.
Only -target android is supported. The 'adb' tool must be on the PATH.
The build flags -a, -i, -n, -x, -gcflags, -ldflags, -tags, -trimpath, and -work are
shared with the build command.
For documentation, see 'go help build'.
`,
}
func runInstall(cmd *command) error {
if !strings.HasPrefix(buildTarget, "android") {
return fmt.Errorf("install is not supported for -target=%s", buildTarget)
}
pkg, err := runBuildImpl(cmd)
if err != nil {
return err
}
// Don't use runCmd as adb does not return a useful exit code.
c := exec.Command(
`adb`,
`install`,
`-r`,
androidPkgName(path.Base(pkg.PkgPath))+`.apk`,
)
c.Stdout = os.Stdout
c.Stderr = os.Stderr
if buildX || buildN {
printcmd("%s", strings.Join(c.Args, " "))
}
if buildN {
return nil
}
return c.Run()
}

209
cmd/gomobile/main.go Normal file
View file

@ -0,0 +1,209 @@
// 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.
package main
//go:generate gomobile help documentation doc.go
import (
"bufio"
"bytes"
"flag"
"fmt"
"html/template"
"io"
"log"
"os"
"os/exec"
"unicode"
"unicode/utf8"
)
var (
gomobileName = "gomobile"
goVersion string
)
const minimumGoMinorVersion = 18
func printUsage(w io.Writer) {
bufw := bufio.NewWriter(w)
if err := usageTmpl.Execute(bufw, commands); err != nil {
panic(err)
}
bufw.Flush()
}
func main() {
gomobileName = os.Args[0]
flag.Usage = func() {
printUsage(os.Stderr)
os.Exit(2)
}
flag.Parse()
log.SetFlags(0)
args := flag.Args()
if len(args) < 1 {
flag.Usage()
}
if args[0] == "help" {
if len(args) == 3 && args[1] == "documentation" {
helpDocumentation(args[2])
return
}
help(args[1:])
return
}
if _, err := ensureGoVersion(); err != nil {
fmt.Fprintf(os.Stderr, "%s: %v\n", gomobileName, err)
os.Exit(1)
}
for _, cmd := range commands {
if cmd.Name == args[0] {
cmd.flag.Usage = func() {
cmd.usage()
os.Exit(1)
}
cmd.flag.Parse(args[1:])
if err := cmd.run(cmd); err != nil {
msg := err.Error()
if msg != "" {
fmt.Fprintf(os.Stderr, "%s: %v\n", os.Args[0], err)
}
os.Exit(1)
}
return
}
}
fmt.Fprintf(os.Stderr, "%s: unknown subcommand %q\nRun 'gomobile help' for usage.\n", os.Args[0], args[0])
os.Exit(2)
}
func ensureGoVersion() (string, error) {
if goVersion != "" {
return goVersion, nil
}
goVersionOut, err := exec.Command("go", "version").CombinedOutput()
if err != nil {
return "", fmt.Errorf("'go version' failed: %v, %s", err, goVersionOut)
}
var minor int
if _, err := fmt.Sscanf(string(goVersionOut), "go version go1.%d", &minor); err != nil {
// Ignore unknown versions; it's probably a devel version.
return "", nil
}
goVersion = fmt.Sprintf("go1.%d", minor)
if minor < minimumGoMinorVersion {
return "", fmt.Errorf("Go 1.%d or newer is required", minimumGoMinorVersion)
}
return goVersion, nil
}
func help(args []string) {
if len(args) == 0 {
printUsage(os.Stdout)
return // succeeded at helping
}
if len(args) != 1 {
fmt.Fprintf(os.Stderr, "usage: %s help command\n\nToo many arguments given.\n", gomobileName)
os.Exit(2) // failed to help
}
arg := args[0]
for _, cmd := range commands {
if cmd.Name == arg {
cmd.usage()
return // succeeded at helping
}
}
fmt.Fprintf(os.Stderr, "Unknown help topic %#q. Run '%s help'.\n", arg, gomobileName)
os.Exit(2)
}
const documentationHeader = `// 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.
// Code generated by 'gomobile help documentation doc.go'. DO NOT EDIT.
`
func helpDocumentation(path string) {
w := new(bytes.Buffer)
w.WriteString(documentationHeader)
w.WriteString("\n/*\n")
if err := usageTmpl.Execute(w, commands); err != nil {
log.Fatal(err)
}
for _, cmd := range commands {
r, rlen := utf8.DecodeRuneInString(cmd.Short)
w.WriteString("\n\n")
w.WriteRune(unicode.ToUpper(r))
w.WriteString(cmd.Short[rlen:])
w.WriteString("\n\nUsage:\n\n\tgomobile " + cmd.Name)
if cmd.Usage != "" {
w.WriteRune(' ')
w.WriteString(cmd.Usage)
}
w.WriteRune('\n')
w.WriteString(cmd.Long)
}
w.WriteString("*/\npackage main // import \"golang.org/x/mobile/cmd/gomobile\"\n")
if err := os.WriteFile(path, w.Bytes(), 0666); err != nil {
log.Fatal(err)
}
}
var commands = []*command{
// TODO(crawshaw): cmdRun
cmdBind,
cmdBuild,
cmdClean,
cmdInit,
cmdInstall,
cmdVersion,
}
type command struct {
run func(*command) error
flag flag.FlagSet
Name string
Usage string
Short string
Long string
}
func (cmd *command) usage() {
fmt.Fprintf(os.Stdout, "usage: %s %s %s\n%s", gomobileName, cmd.Name, cmd.Usage, cmd.Long)
}
var usageTmpl = template.Must(template.New("usage").Parse(
`Gomobile is a tool for building and running mobile apps written in Go.
To install:
$ go install golang.org/x/mobile/cmd/gomobile@latest
$ gomobile init
At least Go 1.16 is required.
For detailed instructions, see https://golang.org/wiki/Mobile.
Usage:
gomobile command [arguments]
Commands:
{{range .}}
{{.Name | printf "%-11s"}} {{.Short}}{{end}}
Use 'gomobile help [command]' for more information about that command.
`))

75
cmd/gomobile/manifest.go Normal file
View file

@ -0,0 +1,75 @@
// 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.
package main
import (
"encoding/xml"
"errors"
"fmt"
"html/template"
)
type manifestXML struct {
Activity activityXML `xml:"application>activity"`
}
type activityXML struct {
Name string `xml:"name,attr"`
MetaData []metaDataXML `xml:"meta-data"`
}
type metaDataXML struct {
Name string `xml:"name,attr"`
Value string `xml:"value,attr"`
}
// manifestLibName parses the AndroidManifest.xml and finds the library
// name of the NativeActivity.
func manifestLibName(data []byte) (string, error) {
manifest := new(manifestXML)
if err := xml.Unmarshal(data, manifest); err != nil {
return "", err
}
if manifest.Activity.Name != "org.golang.app.GoNativeActivity" {
return "", fmt.Errorf("can only build an .apk for GoNativeActivity, not %q", manifest.Activity.Name)
}
libName := ""
for _, md := range manifest.Activity.MetaData {
if md.Name == "android.app.lib_name" {
libName = md.Value
break
}
}
if libName == "" {
return "", errors.New("AndroidManifest.xml missing meta-data android.app.lib_name")
}
return libName, nil
}
type manifestTmplData struct {
JavaPkgPath string
Name string
LibName string
}
var manifestTmpl = template.Must(template.New("manifest").Parse(`
<manifest
xmlns:android="http://schemas.android.com/apk/res/android"
package="{{.JavaPkgPath}}"
android:versionCode="1"
android:versionName="1.0">
<application android:label="{{.Name}}" android:debuggable="true">
<activity android:name="org.golang.app.GoNativeActivity"
android:label="{{.Name}}"
android:configChanges="orientation|keyboardHidden">
<meta-data android:name="android.app.lib_name" android:value="{{.LibName}}" />
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>`))

View file

@ -0,0 +1,62 @@
// 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.
package main
import "fmt"
type stringsFlag []string
func (v *stringsFlag) Set(s string) error {
var err error
*v, err = splitQuotedFields(s)
if *v == nil {
*v = []string{}
}
return err
}
func isSpaceByte(c byte) bool {
return c == ' ' || c == '\t' || c == '\n' || c == '\r'
}
func splitQuotedFields(s string) ([]string, error) {
// Split fields allowing '' or "" around elements.
// Quotes further inside the string do not count.
var f []string
for len(s) > 0 {
for len(s) > 0 && isSpaceByte(s[0]) {
s = s[1:]
}
if len(s) == 0 {
break
}
// Accepted quoted string. No unescaping inside.
if s[0] == '"' || s[0] == '\'' {
quote := s[0]
s = s[1:]
i := 0
for i < len(s) && s[i] != quote {
i++
}
if i >= len(s) {
return nil, fmt.Errorf("unterminated %c string", quote)
}
f = append(f, s[:i])
s = s[i+1:]
continue
}
i := 0
for i < len(s) && !isSpaceByte(s[i]) {
i++
}
f = append(f, s[:i])
s = s[i:]
}
return f, nil
}
func (v *stringsFlag) String() string {
return "<stringsFlag>"
}

9
cmd/gomobile/tools.go Normal file
View file

@ -0,0 +1,9 @@
// This file includes the tools the gomobile depends on.
//go:build tools
package main
import (
_ "golang.org/x/mobile/cmd/gobind"
)

82
cmd/gomobile/version.go Normal file
View file

@ -0,0 +1,82 @@
// 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.
package main
import (
"bytes"
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
"golang.org/x/mobile/internal/sdkpath"
)
var cmdVersion = &command{
run: runVersion,
Name: "version",
Usage: "",
Short: "print version",
Long: `
Version prints versions of the gomobile binary and tools
`,
}
func runVersion(cmd *command) (err error) {
// Check this binary matches the version in golang.org/x/mobile/cmd/gomobile
// source code in GOPATH. If they don't match, currently there is no
// way to reliably identify the revision number this binary was built
// against.
version, err := func() (string, error) {
bin, err := exec.LookPath(os.Args[0])
if err != nil {
return "", err
}
bindir := filepath.Dir(bin)
cmd := exec.Command("go", "list", "-f", "{{.Stale}}", "golang.org/x/mobile/cmd/gomobile")
cmd.Env = append(os.Environ(), "GOBIN="+bindir)
out, err := cmd.CombinedOutput()
if err != nil {
return "", fmt.Errorf("cannot test gomobile binary: %v, %s", err, out)
}
if strings.TrimSpace(string(out)) != "false" {
return "", fmt.Errorf("binary is out of date, re-install it")
}
return mobileRepoRevision()
}()
if err != nil {
fmt.Printf("gomobile version unknown: %v\n", err)
return nil
}
// Supported platforms
platforms := "android"
if xcodeAvailable() {
platforms += "," + strings.Join(applePlatforms, ",")
}
androidapi, _ := sdkpath.AndroidAPIPath(buildAndroidAPI)
fmt.Printf("gomobile version %s (%s); androidSDK=%s\n", version, platforms, androidapi)
return nil
}
func mobileRepoRevision() (rev string, err error) {
b, err := exec.Command("go", "list", "-f", "{{.Dir}}", "golang.org/x/mobile/app").CombinedOutput()
if err != nil {
return "", fmt.Errorf("mobile repo not found: %v, %s", err, b)
}
repo := filepath.Dir(string(b))
if err := os.Chdir(repo); err != nil {
return "", fmt.Errorf("mobile repo %q not accessible: %v", repo, err)
}
revision, err := exec.Command("git", "log", "-n", "1", "--format=format: +%h %cd", "HEAD").CombinedOutput()
if err != nil {
return "", fmt.Errorf("mobile repo git log failed: %v, %s", err, revision)
}
return string(bytes.Trim(revision, " \t\r\n")), nil
}

269
cmd/gomobile/writer.go Normal file
View file

@ -0,0 +1,269 @@
// 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.
package main
// APK is the archival format used for Android apps. It is a ZIP archive with
// three extra files:
//
// META-INF/MANIFEST.MF
// META-INF/CERT.SF
// META-INF/CERT.RSA
//
// The MANIFEST.MF comes from the Java JAR archive format. It is a list of
// files included in the archive along with a SHA1 hash, for example:
//
// Name: lib/armeabi/libbasic.so
// SHA1-Digest: ntLSc1eLCS2Tq1oB4Vw6jvkranw=
//
// For debugging, the equivalent SHA1-Digest can be generated with OpenSSL:
//
// cat lib/armeabi/libbasic.so | openssl sha1 -binary | openssl base64
//
// CERT.SF is a similar manifest. It begins with a SHA1 digest of the entire
// manifest file:
//
// Signature-Version: 1.0
// Created-By: 1.0 (Android)
// SHA1-Digest-Manifest: aJw+u+10C3Enbg8XRCN6jepluYA=
//
// Then for each entry in the manifest it has a SHA1 digest of the manfiest's
// hash combined with the file name:
//
// Name: lib/armeabi/libbasic.so
// SHA1-Digest: Q7NAS6uzrJr6WjePXSGT+vvmdiw=
//
// This can also be generated with openssl:
//
// echo -en "Name: lib/armeabi/libbasic.so\r\nSHA1-Digest: ntLSc1eLCS2Tq1oB4Vw6jvkranw=\r\n\r\n" | openssl sha1 -binary | openssl base64
//
// Note the \r\n line breaks.
//
// CERT.RSA is an RSA signature block made of CERT.SF. Verify it with:
//
// openssl smime -verify -in CERT.RSA -inform DER -content CERT.SF cert.pem
//
// The APK format imposes two extra restrictions on the ZIP format. First,
// it is uncompressed. Second, each contained file is 4-byte aligned. This
// allows the Android OS to mmap contents without unpacking the archive.
// Note: to make life a little harder, Android Studio stores the RSA key used
// for signing in an Oracle Java proprietary keystore format, JKS. For example,
// the generated debug key is in ~/.android/debug.keystore, and can be
// extracted using the JDK's keytool utility:
//
// keytool -importkeystore -srckeystore ~/.android/debug.keystore -destkeystore ~/.android/debug.p12 -deststoretype PKCS12
//
// Once in standard PKCS12, the key can be converted to PEM for use in the
// Go crypto packages:
//
// openssl pkcs12 -in ~/.android/debug.p12 -nocerts -nodes -out ~/.android/debug.pem
//
// Fortunately for debug builds, all that matters is that the APK is signed.
// The choice of key is unimportant, so we can generate one for normal builds.
// For production builds, we can ask users to provide a PEM file.
import (
"archive/zip"
"bytes"
"crypto/rand"
"crypto/rsa"
"crypto/sha1"
"encoding/base64"
"fmt"
"hash"
"io"
)
// NewWriter returns a new Writer writing an APK file to w.
// The APK will be signed with key.
func NewWriter(w io.Writer, priv *rsa.PrivateKey) *Writer {
apkw := &Writer{priv: priv}
apkw.w = zip.NewWriter(&countWriter{apkw: apkw, w: w})
return apkw
}
// Writer implements an APK file writer.
type Writer struct {
offset int
w *zip.Writer
priv *rsa.PrivateKey
manifest []manifestEntry
cur *fileWriter
}
// Create adds a file to the APK archive using the provided name.
//
// The name must be a relative path. The file's contents must be written to
// the returned io.Writer before the next call to Create or Close.
func (w *Writer) Create(name string) (io.Writer, error) {
if err := w.clearCur(); err != nil {
return nil, fmt.Errorf("apk: Create(%s): %v", name, err)
}
res, err := w.create(name)
if err != nil {
return nil, fmt.Errorf("apk: Create(%s): %v", name, err)
}
return res, nil
}
func (w *Writer) create(name string) (io.Writer, error) {
// Align start of file contents by using Extra as padding.
if err := w.w.Flush(); err != nil { // for exact offset
return nil, err
}
const fileHeaderLen = 30 // + filename + extra
start := w.offset + fileHeaderLen + len(name)
extra := (-start) & 3
zipfw, err := w.w.CreateHeader(&zip.FileHeader{
Name: name,
Extra: make([]byte, extra),
})
if err != nil {
return nil, err
}
w.cur = &fileWriter{
name: name,
w: zipfw,
sha1: sha1.New(),
}
return w.cur, nil
}
// Close finishes writing the APK. This includes writing the manifest and
// signing the archive, and writing the ZIP central directory.
//
// It does not close the underlying writer.
func (w *Writer) Close() error {
if err := w.clearCur(); err != nil {
return fmt.Errorf("apk: %v", err)
}
hasDex := false
for _, entry := range w.manifest {
if entry.name == "classes.dex" {
hasDex = true
break
}
}
manifest := new(bytes.Buffer)
if hasDex {
fmt.Fprint(manifest, manifestDexHeader)
} else {
fmt.Fprint(manifest, manifestHeader)
}
certBody := new(bytes.Buffer)
for _, entry := range w.manifest {
n := entry.name
h := base64.StdEncoding.EncodeToString(entry.sha1.Sum(nil))
fmt.Fprintf(manifest, "Name: %s\nSHA1-Digest: %s\n\n", n, h)
cHash := sha1.New()
fmt.Fprintf(cHash, "Name: %s\r\nSHA1-Digest: %s\r\n\r\n", n, h)
ch := base64.StdEncoding.EncodeToString(cHash.Sum(nil))
fmt.Fprintf(certBody, "Name: %s\nSHA1-Digest: %s\n\n", n, ch)
}
mHash := sha1.New()
mHash.Write(manifest.Bytes())
cert := new(bytes.Buffer)
fmt.Fprint(cert, certHeader)
fmt.Fprintf(cert, "SHA1-Digest-Manifest: %s\n\n", base64.StdEncoding.EncodeToString(mHash.Sum(nil)))
cert.Write(certBody.Bytes())
mw, err := w.Create("META-INF/MANIFEST.MF")
if err != nil {
return err
}
if _, err := mw.Write(manifest.Bytes()); err != nil {
return err
}
cw, err := w.Create("META-INF/CERT.SF")
if err != nil {
return err
}
if _, err := cw.Write(cert.Bytes()); err != nil {
return err
}
rsa, err := signPKCS7(rand.Reader, w.priv, cert.Bytes())
if err != nil {
return fmt.Errorf("apk: %v", err)
}
rw, err := w.Create("META-INF/CERT.RSA")
if err != nil {
return err
}
if _, err := rw.Write(rsa); err != nil {
return err
}
return w.w.Close()
}
const manifestHeader = `Manifest-Version: 1.0
Created-By: 1.0 (Go)
`
const manifestDexHeader = `Manifest-Version: 1.0
Dex-Location: classes.dex
Created-By: 1.0 (Go)
`
const certHeader = `Signature-Version: 1.0
Created-By: 1.0 (Go)
`
func (w *Writer) clearCur() error {
if w.cur == nil {
return nil
}
w.manifest = append(w.manifest, manifestEntry{
name: w.cur.name,
sha1: w.cur.sha1,
})
w.cur.closed = true
w.cur = nil
return nil
}
type manifestEntry struct {
name string
sha1 hash.Hash
}
type countWriter struct {
apkw *Writer
w io.Writer
}
func (c *countWriter) Write(p []byte) (n int, err error) {
n, err = c.w.Write(p)
c.apkw.offset += n
return n, err
}
type fileWriter struct {
name string
w io.Writer
sha1 hash.Hash
closed bool
}
func (w *fileWriter) Write(p []byte) (n int, err error) {
if w.closed {
return 0, fmt.Errorf("apk: write to closed file %q", w.name)
}
w.sha1.Write(p)
n, err = w.w.Write(p)
if err != nil {
err = fmt.Errorf("apk: %v", err)
}
return n, err
}

175
cmd/gomobile/writer_test.go Normal file
View file

@ -0,0 +1,175 @@
// 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.
package main
import (
"crypto/x509"
"encoding/pem"
"io"
"os"
"os/exec"
"testing"
)
func TestWriter(t *testing.T) {
if os.Getenv("GO_BUILDER_NAME") == "linux-amd64-androidemu" {
t.Skip("skipping on linux-amd64-androidemu builder; see golang.org/issue/40290")
}
block, _ := pem.Decode([]byte(debugCert))
if block == nil {
t.Fatal("no cert")
}
privKey, err := x509.ParsePKCS1PrivateKey(block.Bytes)
if err != nil {
t.Fatal(err)
}
f, err := os.CreateTemp("", "testapk-")
if err != nil {
t.Fatal(err)
}
f.Close()
defer os.Remove(f.Name())
apkPath := f.Name() + ".apk"
f, err = os.Create(apkPath)
if err != nil {
t.Fatal(err)
}
defer os.Remove(apkPath)
apkw := NewWriter(f, privKey)
w, err := apkw.Create("AndroidManifest.xml")
if err != nil {
t.Fatalf("could not create AndroidManifest.xml: %v", err)
}
if _, err := w.Write([]byte(androidManifest)); err != nil {
t.Errorf("could not write AndroidManifest.xml: %v", err)
}
w, err = apkw.Create("assets/hello_world.txt")
if err != nil {
t.Fatalf("could not create assets/hello_world.txt: %v", err)
}
if _, err := w.Write([]byte("Hello, 世界")); err != nil {
t.Errorf("could not write assets/hello_world.txt: %v", err)
}
if err := apkw.Close(); err != nil {
t.Fatal(err)
}
if exec.Command("which", "aapt").Run() != nil {
t.Skip("command aapt not found, skipping")
}
out, err := exec.Command("aapt", "list", "-a", apkPath).CombinedOutput()
aaptGot := string(out)
if err != nil {
t.Logf("aapt:\n%s", aaptGot)
t.Fatalf("aapt failed: %v", err)
}
if aaptGot != aaptWant {
t.Errorf("unexpected output from aapt")
d, err := diff(aaptGot, aaptWant)
if err != nil {
t.Errorf("diff failed: %v", err)
} else {
t.Logf("%s", d)
}
}
}
const aaptWant = `AndroidManifest.xml
assets/hello_world.txt
META-INF/MANIFEST.MF
META-INF/CERT.SF
META-INF/CERT.RSA
Resource table:
Package Groups (0)
Android manifest:
N: android=http://schemas.android.com/apk/res/android
E: manifest (line=2)
A: package="org.golang.fakeapp" (Raw: "org.golang.fakeapp")
A: android:versionCode(0x0101021b)=(type 0x10)0x1
A: android:versionName(0x0101021c)="1.0" (Raw: "1.0")
E: uses-sdk (line=8)
A: android:minSdkVersion(0x0101020c)=(type 0x10)0xf
E: application (line=9)
A: android:label(0x01010001)="FakeApp" (Raw: "FakeApp")
A: android:hasCode(0x0101000c)=(type 0x12)0x0
A: android:debuggable(0x0101000f)=(type 0x12)0xffffffff
E: activity (line=10)
A: android:name(0x01010003)="android.app.NativeActivity" (Raw: "android.app.NativeActivity")
A: android:label(0x01010001)="FakeApp" (Raw: "FakeApp")
A: android:configChanges(0x0101001f)=(type 0x11)0xa0
E: intent-filter (line=14)
E: action (line=15)
A: android:name(0x01010003)="android.intent.action.MAIN" (Raw: "android.intent.action.MAIN")
E: category (line=16)
A: android:name(0x01010003)="android.intent.category.LAUNCHER" (Raw: "android.intent.category.LAUNCHER")
`
const androidManifest = `
<manifest
xmlns:android="http://schemas.android.com/apk/res/android"
package="org.golang.fakeapp"
android:versionCode="1"
android:versionName="1.0">
<application android:label="FakeApp" android:hasCode="false" android:debuggable="true">
<activity android:name="android.app.NativeActivity"
android:label="FakeApp"
android:configChanges="orientation|keyboardHidden">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
`
func writeTempFile(data string) (string, error) {
f, err := os.CreateTemp("", "gofmt")
if err != nil {
return "", err
}
_, err = io.WriteString(f, data)
errc := f.Close()
if err == nil {
return f.Name(), errc
}
return f.Name(), err
}
func diff(got, want string) (string, error) {
wantPath, err := writeTempFile(want)
if err != nil {
return "", err
}
defer os.Remove(wantPath)
gotPath, err := writeTempFile(got)
if err != nil {
return "", err
}
defer os.Remove(gotPath)
data, err := exec.Command("diff", "-u", wantPath, gotPath).CombinedOutput()
if len(data) > 0 {
// diff exits with a non-zero status when the files don't match.
// Ignore that failure as long as we get output.
err = nil
}
return string(data), err
}