Adding upstream version 0.0~git20250520.a1d9079+dfsg.
Signed-off-by: Daniel Baumann <daniel@debian.org>
This commit is contained in:
parent
590ac7ff5f
commit
20149b7f3a
456 changed files with 70406 additions and 0 deletions
252
cmd/gobind/doc.go
Normal file
252
cmd/gobind/doc.go
Normal 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
391
cmd/gobind/gen.go
Normal 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
218
cmd/gobind/gobind_test.go
Normal 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
11
cmd/gobind/implicit.go
Normal 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
168
cmd/gobind/main.go
Normal 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
355
cmd/gomobile/bind.go
Normal 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
|
||||
}
|
400
cmd/gomobile/bind_androidapp.go
Normal file
400
cmd/gomobile/bind_androidapp.go
Normal 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
398
cmd/gomobile/bind_iosapp.go
Normal 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
404
cmd/gomobile/bind_test.go
Normal 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
448
cmd/gomobile/build.go
Normal 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
|
||||
}
|
357
cmd/gomobile/build_androidapp.go
Normal file
357
cmd/gomobile/build_androidapp.go
Normal 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
617
cmd/gomobile/build_apple.go
Normal 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 (A–Z, a–z),
|
||||
// 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)
|
||||
}
|
123
cmd/gomobile/build_darwin_test.go
Normal file
123
cmd/gomobile/build_darwin_test.go
Normal 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
283
cmd/gomobile/build_test.go
Normal 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
154
cmd/gomobile/cert.go
Normal 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
101
cmd/gomobile/cert_test.go
Normal 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
32
cmd/gomobile/clean.go
Normal 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
51
cmd/gomobile/dex.go
Normal 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
170
cmd/gomobile/doc.go
Normal 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
626
cmd/gomobile/env.go
Normal 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 isn’t 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
160
cmd/gomobile/env_test.go
Normal 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
158
cmd/gomobile/gendex.go
Normal 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
354
cmd/gomobile/init.go
Normal 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
203
cmd/gomobile/init_test.go
Normal 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
57
cmd/gomobile/install.go
Normal 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
209
cmd/gomobile/main.go
Normal 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
75
cmd/gomobile/manifest.go
Normal 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>`))
|
62
cmd/gomobile/strings_flag.go
Normal file
62
cmd/gomobile/strings_flag.go
Normal 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
9
cmd/gomobile/tools.go
Normal 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
82
cmd/gomobile/version.go
Normal 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
269
cmd/gomobile/writer.go
Normal 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
175
cmd/gomobile/writer_test.go
Normal 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
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue