310 lines
7.4 KiB
Go
310 lines
7.4 KiB
Go
package export
|
|
|
|
import (
|
|
"fmt"
|
|
"image"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strings"
|
|
"time"
|
|
|
|
"aego/app/assembly/builtin"
|
|
"aego/app/bootstrap"
|
|
"aego/app/export/icon"
|
|
"aego/core/errs"
|
|
"aego/core/log"
|
|
"aego/project"
|
|
"aego/runtime/bundle"
|
|
)
|
|
|
|
const Owner = "export"
|
|
|
|
type Options struct {
|
|
Project *project.Project
|
|
Engine string
|
|
Target string
|
|
Profile string
|
|
Out string
|
|
CC string
|
|
CheckOnly bool
|
|
Snapshot *Snapshot
|
|
Log io.Writer
|
|
Progress func(float32, string)
|
|
}
|
|
|
|
type Target struct {
|
|
OS string
|
|
Arch string
|
|
}
|
|
|
|
func (t Target) String() string {
|
|
if t.Web() {
|
|
return "web"
|
|
}
|
|
return t.OS + "/" + t.Arch
|
|
}
|
|
|
|
func (t Target) Web() bool {
|
|
return t.OS == "js"
|
|
}
|
|
|
|
func (t Target) Host() bool {
|
|
return t.OS == runtime.GOOS && t.Arch == runtime.GOARCH
|
|
}
|
|
|
|
func (t Target) Exe(name string) string {
|
|
if t.OS == "windows" {
|
|
return name + ".exe"
|
|
}
|
|
return name
|
|
}
|
|
|
|
func Targets() []string {
|
|
return []string{"host", "linux", "windows", "darwin", "web"}
|
|
}
|
|
|
|
func ParseTarget(s string) (Target, error) {
|
|
s = strings.ToLower(strings.TrimSpace(s))
|
|
if s == "" || s == "host" {
|
|
return Target{OS: runtime.GOOS, Arch: runtime.GOARCH}, nil
|
|
}
|
|
switch s {
|
|
case "web", "js", "wasm", "js/wasm":
|
|
return Target{OS: "js", Arch: "wasm"}, nil
|
|
}
|
|
osName, arch, _ := strings.Cut(s, "/")
|
|
if arch == "" {
|
|
arch = runtime.GOARCH
|
|
}
|
|
switch osName {
|
|
case "macos":
|
|
osName = "darwin"
|
|
case "linux", "windows", "darwin":
|
|
default:
|
|
return Target{}, errs.New(errs.EngineConfig, "unknown export target "+s, log.F("targets", strings.Join(Targets(), ", ")))
|
|
}
|
|
switch arch {
|
|
case "amd64", "arm64":
|
|
default:
|
|
return Target{}, errs.New(errs.EngineConfig, "unsupported architecture "+arch+", use amd64 or arm64")
|
|
}
|
|
return Target{OS: osName, Arch: arch}, nil
|
|
}
|
|
|
|
func ParseProfile(s string) (string, error) {
|
|
switch strings.ToLower(strings.TrimSpace(s)) {
|
|
case "", "dev", bundle.Development:
|
|
return bundle.Development, nil
|
|
case bundle.Release:
|
|
return bundle.Release, nil
|
|
}
|
|
return "", errs.New(errs.EngineConfig, "profile must be development or release", log.F("profile", s))
|
|
}
|
|
|
|
func DefaultOut(p *project.Project, t Target, profile string) string {
|
|
if t.Web() {
|
|
return filepath.Join(p.Root, "exports", "web-"+profile)
|
|
}
|
|
return filepath.Join(p.Root, "exports", t.OS+"-"+t.Arch+"-"+profile)
|
|
}
|
|
|
|
func ExeName(p *project.Project) string {
|
|
return project.ModuleName(p.Name)
|
|
}
|
|
|
|
func AppName(p *project.Project) string {
|
|
n := strings.TrimSpace(strings.NewReplacer("/", "-", ":", "-", "\\", "-").Replace(p.Name))
|
|
if n == "" {
|
|
return ExeName(p)
|
|
}
|
|
return n
|
|
}
|
|
|
|
func GameVersion(p *project.Project) string {
|
|
if p.GameVersion != "" {
|
|
return p.GameVersion
|
|
}
|
|
return "0.0.0"
|
|
}
|
|
|
|
type layout struct {
|
|
root string
|
|
bin string
|
|
data string
|
|
app string
|
|
exe string
|
|
}
|
|
|
|
func layoutFor(t Target, out string, p *project.Project) layout {
|
|
if t.Web() {
|
|
return layout{root: out, bin: out, data: filepath.Join(out, bundle.DataDir), exe: WebWasm}
|
|
}
|
|
exe := t.Exe(ExeName(p))
|
|
if t.OS == "darwin" {
|
|
app := filepath.Join(out, AppName(p)+".app")
|
|
return layout{root: out, app: app, exe: exe,
|
|
bin: filepath.Join(app, "Contents", "MacOS"),
|
|
data: filepath.Join(app, "Contents", "Resources", bundle.DataDir)}
|
|
}
|
|
return layout{root: out, bin: out, data: filepath.Join(out, bundle.DataDir), exe: exe}
|
|
}
|
|
|
|
func (l layout) binary() string {
|
|
return filepath.Join(l.bin, l.exe)
|
|
}
|
|
|
|
type job struct {
|
|
opt Options
|
|
p *project.Project
|
|
t Target
|
|
profile string
|
|
out string
|
|
l layout
|
|
native bool
|
|
snap *Snapshot
|
|
icon image.Image
|
|
rep *Report
|
|
start time.Time
|
|
goroot string
|
|
files []webFile
|
|
manifestSize int64
|
|
}
|
|
|
|
func Export(opt Options) (*Report, error) {
|
|
p := opt.Project
|
|
if p == nil {
|
|
return nil, errs.New(errs.ProjectNotFound, "export needs a project")
|
|
}
|
|
t, err := ParseTarget(opt.Target)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
profile, err := ParseProfile(opt.Profile)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
out := opt.Out
|
|
if out == "" {
|
|
out = DefaultOut(p, t, profile)
|
|
}
|
|
if out, err = filepath.Abs(out); err != nil {
|
|
return nil, errs.Wrap(err, errs.VFSBadPath, "cannot resolve the output directory")
|
|
}
|
|
j := &job{opt: opt, p: p, t: t, profile: profile, out: out, l: layoutFor(t, out, p),
|
|
native: bootstrap.NeedsNative(p), snap: opt.Snapshot, start: time.Now()}
|
|
j.rep = &Report{Target: t.String(), Profile: profile, Out: out, Modules: j.modules()}
|
|
err = j.run()
|
|
j.rep.Millis = time.Since(j.start).Milliseconds()
|
|
if err != nil {
|
|
j.rep.Error = err.Error()
|
|
}
|
|
return j.rep, err
|
|
}
|
|
|
|
func (j *job) run() error {
|
|
j.step(0.02, "checking the project")
|
|
if j.snap == nil {
|
|
snap, done, err := Fresh(j.p)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer done()
|
|
j.snap = snap
|
|
}
|
|
ds := Check(j.p, j.snap, j.profile, j.native)
|
|
if j.t.Web() {
|
|
ds = append(ds, CheckWeb(j.snap, j.native)...)
|
|
}
|
|
j.rep.setDiagnostics(ds)
|
|
if j.rep.Blocked {
|
|
return errs.New(errs.BuildFailed, fmt.Sprintf("export is blocked by %d problem(s)", Blocking(ds)))
|
|
}
|
|
if j.opt.CheckOnly {
|
|
return nil
|
|
}
|
|
if j.p.Icon != "" {
|
|
img, err := icon.Load(filepath.Join(j.p.Root, filepath.FromSlash(j.p.Icon)))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
j.icon = img
|
|
}
|
|
if err := j.toolchain(); err != nil {
|
|
return err
|
|
}
|
|
if err := j.prepareOut(); err != nil {
|
|
return err
|
|
}
|
|
j.step(0.1, "building the runtime for "+j.t.String())
|
|
if err := j.build(); err != nil {
|
|
return err
|
|
}
|
|
j.step(0.7, "copying the game data")
|
|
if err := j.data(); err != nil {
|
|
return err
|
|
}
|
|
j.step(0.85, "packaging for "+j.t.String())
|
|
if err := j.pack(); err != nil {
|
|
return err
|
|
}
|
|
j.step(0.95, "writing the export manifest")
|
|
if err := j.manifest(); err != nil {
|
|
return err
|
|
}
|
|
j.step(1, "done")
|
|
return nil
|
|
}
|
|
|
|
func (j *job) step(f float32, msg string) {
|
|
if j.opt.Progress != nil {
|
|
j.opt.Progress(f, msg)
|
|
}
|
|
if j.opt.Log != nil {
|
|
fmt.Fprintf(j.opt.Log, "export: %s\n", msg)
|
|
}
|
|
}
|
|
|
|
func (j *job) modules() []string {
|
|
out := builtin.Runtime().IDs()
|
|
if j.native {
|
|
out = append(out, "native:"+j.p.Module)
|
|
}
|
|
return out
|
|
}
|
|
|
|
func inside(parent, child string) bool {
|
|
rel, err := filepath.Rel(parent, child)
|
|
return err == nil && rel != ".." && !strings.HasPrefix(rel, ".."+string(filepath.Separator))
|
|
}
|
|
|
|
func (j *job) prepareOut() error {
|
|
p := j.p
|
|
if inside(j.out, p.Root) {
|
|
return errs.New(errs.VFSBadPath, "the output directory must not contain the project", log.F("dir", j.out))
|
|
}
|
|
for _, d := range []string{p.AssetsDir(), p.ScenesDir(), p.ScriptsDir(), p.PrefabsDir(), p.NativeDir(), filepath.Join(p.Root, ".aego")} {
|
|
if inside(d, j.out) {
|
|
return errs.New(errs.VFSBadPath, "the output directory must not be inside a project source folder", log.F("dir", j.out))
|
|
}
|
|
}
|
|
entries, err := os.ReadDir(j.out)
|
|
switch {
|
|
case os.IsNotExist(err):
|
|
case err != nil:
|
|
return errs.Wrap(err, errs.VFSIO, "cannot read the output directory").With(log.F("dir", j.out))
|
|
case len(entries) > 0:
|
|
if _, err := os.Stat(filepath.Join(j.out, bundle.ExportFile)); err != nil {
|
|
return errs.New(errs.ProjectExists, "the output directory is not empty and holds no previous export", log.F("dir", j.out))
|
|
}
|
|
if err := os.RemoveAll(j.out); err != nil {
|
|
return errs.Wrap(err, errs.VFSIO, "cannot clear the previous export").With(log.F("dir", j.out))
|
|
}
|
|
}
|
|
if err := os.MkdirAll(j.l.data, 0o755); err != nil {
|
|
return errs.Wrap(err, errs.VFSIO, "cannot create the output directory").With(log.F("dir", j.out))
|
|
}
|
|
return nil
|
|
}
|