main.go 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364
  1. // Copyright 2019 The Ebiten Authors
  2. //
  3. // Licensed under the Apache License, Version 2.0 (the "License");
  4. // you may not use this file except in compliance with the License.
  5. // You may obtain a copy of the License at
  6. //
  7. // http://www.apache.org/licenses/LICENSE-2.0
  8. //
  9. // Unless required by applicable law or agreed to in writing, software
  10. // distributed under the License is distributed on an "AS IS" BASIS,
  11. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. // See the License for the specific language governing permissions and
  13. // limitations under the License.
  14. // ebitenmobile is a wrapper of gomobile for Ebitengine.
  15. //
  16. // For the usage, see https://ebitengine.org/en/documents/mobile.html.
  17. //
  18. // ebitenmobile uses github.com/ebitengine/gomobile for gomobile, not golang.org/x/mobile.
  19. // gomobile's version is fixed by ebitenmobile.
  20. // You can specify gomobile's version by EBITENMOBILE_GOMOBILE environment variable.
  21. package main
  22. import (
  23. _ "embed"
  24. "flag"
  25. "fmt"
  26. "log"
  27. "os"
  28. "os/exec"
  29. "path/filepath"
  30. "strings"
  31. "text/template"
  32. "unicode"
  33. "golang.org/x/tools/go/packages"
  34. )
  35. const (
  36. ebitenmobileCommand = "ebitenmobile"
  37. )
  38. //go:embed _files/EbitenViewController.h
  39. var objcH string
  40. func goEnv(name string) string {
  41. if val := os.Getenv(name); val != "" {
  42. return val
  43. }
  44. val, err := exec.Command("go", "env", name).Output()
  45. if err != nil {
  46. panic(err)
  47. }
  48. return strings.TrimSpace(string(val))
  49. }
  50. var (
  51. buildA bool // -a
  52. buildI bool // -i
  53. buildN bool // -n
  54. buildV bool // -v
  55. buildX bool // -x
  56. buildO string // -o
  57. buildGcflags string // -gcflags
  58. buildLdflags string // -ldflags
  59. buildTarget string // -target
  60. buildTrimpath bool // -trimpath
  61. buildWork bool // -work
  62. buildBundleID string // -bundleid
  63. buildIOSVersion string // -iosversion
  64. buildAndroidAPI int // -androidapi
  65. buildTags string // -tags
  66. bindPrefix string // -prefix
  67. bindJavaPkg string // -javapkg
  68. bindClasspath string // -classpath
  69. bindBootClasspath string // -bootclasspath
  70. )
  71. func main() {
  72. flag.Usage = func() {
  73. // This message is copied from `gomobile bind -h`
  74. fmt.Fprintf(os.Stderr, "%s bind [-target android|ios] [-bootclasspath <path>] [-classpath <path>] [-o output] [build flags] [package]\n", ebitenmobileCommand)
  75. os.Exit(2)
  76. }
  77. flag.Parse()
  78. args := flag.Args()
  79. if len(args) < 1 {
  80. flag.Usage()
  81. }
  82. if args[0] != "bind" {
  83. flag.Usage()
  84. }
  85. // minAndroidAPI specifies the minimum API version for Android.
  86. // Now Google Player v23.30.99+ drops API levels that are older than 21.
  87. // See https://apilevels.com/.
  88. const minAndroidAPI = 21
  89. var flagset flag.FlagSet
  90. flagset.StringVar(&buildO, "o", "", "")
  91. flagset.StringVar(&buildGcflags, "gcflags", "", "")
  92. flagset.StringVar(&buildLdflags, "ldflags", "", "")
  93. flagset.StringVar(&buildTarget, "target", "android", "")
  94. flagset.StringVar(&buildBundleID, "bundleid", "", "")
  95. flagset.StringVar(&buildIOSVersion, "iosversion", "7.0", "")
  96. flagset.StringVar(&buildTags, "tags", "", "")
  97. flagset.IntVar(&buildAndroidAPI, "androidapi", minAndroidAPI, "")
  98. flagset.BoolVar(&buildA, "a", false, "")
  99. flagset.BoolVar(&buildI, "i", false, "")
  100. flagset.BoolVar(&buildN, "n", false, "")
  101. flagset.BoolVar(&buildV, "v", false, "")
  102. flagset.BoolVar(&buildX, "x", false, "")
  103. flagset.BoolVar(&buildTrimpath, "trimpath", false, "")
  104. flagset.BoolVar(&buildWork, "work", false, "")
  105. flagset.StringVar(&bindJavaPkg, "javapkg", "", "")
  106. flagset.StringVar(&bindPrefix, "prefix", "", "")
  107. flagset.StringVar(&bindClasspath, "classpath", "", "")
  108. flagset.StringVar(&bindBootClasspath, "bootclasspath", "", "")
  109. _ = flagset.Parse(args[1:])
  110. buildTarget, err := osFromBuildTarget(buildTarget)
  111. if err != nil {
  112. log.Fatal(err)
  113. }
  114. // Add ldflags to suppress linker errors (#932).
  115. // See https://github.com/golang/go/issues/17807
  116. if buildTarget == "android" {
  117. if buildLdflags != "" {
  118. buildLdflags += " "
  119. }
  120. buildLdflags += "-extldflags=-Wl,-soname,libgojni.so"
  121. if !isValidJavaPackageName(bindJavaPkg) {
  122. log.Fatalf("invalid Java package name: %s", bindJavaPkg)
  123. }
  124. }
  125. dir, err := prepareGomobileCommands()
  126. defer func() {
  127. if dir != "" && !buildWork {
  128. _ = removeAll(dir)
  129. }
  130. }()
  131. if err != nil {
  132. log.Fatal(err)
  133. }
  134. // If args doesn't include '-androidapi', set it to args explicitly.
  135. // It's because ebitenmobile's default API level is different from gomobile's one.
  136. if buildTarget == "android" && buildAndroidAPI == minAndroidAPI {
  137. var found bool
  138. flag.Visit(func(f *flag.Flag) {
  139. if f.Name == "androidapi" {
  140. found = true
  141. }
  142. })
  143. if !found {
  144. args = append([]string{args[0], "-androidapi", fmt.Sprintf("%d", minAndroidAPI)}, args[1:]...)
  145. }
  146. }
  147. if err := doBind(args, &flagset, buildTarget); err != nil {
  148. log.Fatal(err)
  149. }
  150. }
  151. func osFromBuildTarget(buildTarget string) (string, error) {
  152. var os string
  153. for i, pair := range strings.Split(buildTarget, ",") {
  154. osarch := strings.SplitN(pair, "/", 2)
  155. if i == 0 {
  156. os = osarch[0]
  157. }
  158. if os != osarch[0] {
  159. return "", fmt.Errorf("ebitenmobile: cannot target different OSes")
  160. }
  161. }
  162. if os == "ios" {
  163. os = "darwin"
  164. }
  165. return os, nil
  166. }
  167. func doBind(args []string, flagset *flag.FlagSet, buildOS string) error {
  168. tags := buildTags
  169. cfg := &packages.Config{}
  170. cfg.Env = append(os.Environ(), "GOOS="+buildOS)
  171. if buildOS == "darwin" {
  172. if tags != "" {
  173. tags += " "
  174. }
  175. tags += "ios"
  176. }
  177. cfg.BuildFlags = []string{"-tags", tags}
  178. flagsetArgs := flagset.Args()
  179. if len(flagsetArgs) == 0 {
  180. flagsetArgs = []string{"."}
  181. }
  182. pkgs, err := packages.Load(cfg, flagsetArgs[0])
  183. if err != nil {
  184. return err
  185. }
  186. prefixLower := bindPrefix + pkgs[0].Name
  187. prefixUpper := strings.Title(bindPrefix) + strings.Title(pkgs[0].Name)
  188. args = append(args, "github.com/hajimehoshi/ebiten/v2/mobile/ebitenmobileview")
  189. if buildO == "" {
  190. fmt.Fprintln(os.Stderr, "-o must be specified.")
  191. os.Exit(2)
  192. return nil
  193. }
  194. if buildN {
  195. fmt.Print("gomobile")
  196. for _, arg := range args {
  197. fmt.Print(" ", arg)
  198. }
  199. fmt.Println()
  200. return nil
  201. }
  202. cmd := exec.Command("gomobile", args...)
  203. cmd.Stdout = os.Stdout
  204. cmd.Stderr = os.Stderr
  205. if err := cmd.Run(); err != nil {
  206. os.Exit(err.(*exec.ExitError).ExitCode())
  207. return nil
  208. }
  209. replacePrefixes := func(content string) string {
  210. content = strings.ReplaceAll(content, "{{.PrefixUpper}}", prefixUpper)
  211. content = strings.ReplaceAll(content, "{{.PrefixLower}}", prefixLower)
  212. return content
  213. }
  214. if buildOS == "darwin" {
  215. // TODO: Use os.ReadDir after Ebitengine stops supporting Go 1.15.
  216. f, err := os.Open(buildO)
  217. if err != nil {
  218. return err
  219. }
  220. defer func() {
  221. _ = f.Close()
  222. }()
  223. names, err := f.Readdirnames(-1)
  224. if err != nil {
  225. return err
  226. }
  227. for _, name := range names {
  228. if name == "Info.plist" {
  229. continue
  230. }
  231. frameworkName := filepath.Base(buildO)
  232. frameworkNameBase := frameworkName[:len(frameworkName)-len(".xcframework")]
  233. // The first character must be an upper case (#2192).
  234. // TODO: strings.Title is used here for the consistency with gomobile (see cmd/gomobile/bind_iosapp.go).
  235. // As strings.Title is deprecated, golang.org/x/text/cases should be used.
  236. frameworkNameBase = strings.Title(frameworkNameBase)
  237. dir := filepath.Join(buildO, name, frameworkNameBase+".framework")
  238. if err := os.WriteFile(filepath.Join(dir, "Headers", prefixUpper+"EbitenViewController.h"), []byte(replacePrefixes(objcH)), 0644); err != nil {
  239. return err
  240. }
  241. // TODO: Remove 'Ebitenmobileview.objc.h' here. Now it is hard since there is a header file importing
  242. // that header file.
  243. fs, err := os.ReadDir(filepath.Join(dir, "Headers"))
  244. if err != nil {
  245. return err
  246. }
  247. var headerFiles []string
  248. for _, f := range fs {
  249. if strings.HasSuffix(f.Name(), ".h") {
  250. headerFiles = append(headerFiles, f.Name())
  251. }
  252. }
  253. w, err := os.OpenFile(filepath.Join(dir, "Modules", "module.modulemap"), os.O_WRONLY, 0644)
  254. if err != nil {
  255. return err
  256. }
  257. defer func() {
  258. _ = w.Close()
  259. }()
  260. var mmVals = struct {
  261. Module string
  262. Headers []string
  263. }{
  264. Module: prefixUpper,
  265. Headers: headerFiles,
  266. }
  267. if err := iosModuleMapTmpl.Execute(w, mmVals); err != nil {
  268. return err
  269. }
  270. // TODO: Remove Ebitenmobileview.objc.h?
  271. }
  272. }
  273. return nil
  274. }
  275. var iosModuleMapTmpl = template.Must(template.New("iosmmap").Parse(`framework module "{{.Module}}" {
  276. {{range .Headers}} header "{{.}}"
  277. {{end}}
  278. export *
  279. }`))
  280. func isValidJavaPackageName(name string) bool {
  281. if name == "" {
  282. return false
  283. }
  284. // A Java package name consists of one or more Java identifiers separated by dots.
  285. for _, token := range strings.Split(name, ".") {
  286. if !isValidJavaIdentifier(token) {
  287. return false
  288. }
  289. }
  290. return true
  291. }
  292. // isValidJavaIdentifier reports whether the given strings is a valid Java identifier.
  293. func isValidJavaIdentifier(name string) bool {
  294. if name == "" {
  295. return false
  296. }
  297. // Java identifiers must not be a Java keyword or a boolean/null literal.
  298. // https://docs.oracle.com/javase/specs/jls/se21/html/jls-3.html#jls-Identifier
  299. switch name {
  300. case "_", "abstract", "assert", "boolean", "break", "byte", "case", "catch", "char", "class", "const", "continue", "default", "do", "double", "else", "enum", "extends", "final", "finally", "float", "for", "goto", "if", "implements", "import", "instanceof", "int", "interface", "long", "native", "new", "package", "private", "protected", "public", "return", "short", "static", "strictfp", "super", "switch", "synchronized", "this", "throw", "throws", "transient", "try", "void", "volatile", "while":
  301. return false
  302. }
  303. if name == "null" || name == "true" || name == "false" {
  304. return false
  305. }
  306. // References:
  307. // * https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/lang/Character.html#isJavaIdentifierPart(int)
  308. // * https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/lang/Character.html#isJavaIdentifierStart(int)
  309. isJavaLetter := func(r rune) bool {
  310. return unicode.IsLetter(r) || unicode.Is(unicode.Pc, r) || unicode.Is(unicode.Sc, r)
  311. }
  312. isJavaDigit := unicode.IsDigit
  313. // A Java identifier is a Java letter or Java letter followed by Java letters or Java digits.
  314. // https://docs.oracle.com/javase/specs/jls/se21/html/jls-3.html#jls-Identifier
  315. for i, r := range name {
  316. if !isJavaLetter(r) && (i == 0 || !isJavaDigit(r)) {
  317. return false
  318. }
  319. }
  320. return true
  321. }