Skip to content

Commit

Permalink
all: refactoring
Browse files Browse the repository at this point in the history
  • Loading branch information
eihigh committed May 31, 2024
1 parent f5ea1d9 commit 0a2bec9
Show file tree
Hide file tree
Showing 6 changed files with 347 additions and 173 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,4 +89,4 @@ In this case, if the aspect ratio of the game area specified in HTML does not ma
### License
This repository is licensed under the BSD Zero Clause License (0BSD). When duplicating or forking this repository, you may omit or delete the original license notice.

`tool/serve.go` is a modified version of the code from github.com/hajimehoshi/wasmserve licensed under the Apache License 2.0.
`tool/serve.go` and `tool/build.go` are modified version of the code from github.com/hajimehoshi/wasmserve licensed under the Apache License 2.0.
2 changes: 1 addition & 1 deletion README_ja.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,4 +89,4 @@ Ebitengine の仕様上、ブラウザゲームとしてビルドすると、`Se
### ライセンス
このリポジトリはBSD Zero Clause License (0BSD) の下でライセンスされています。このリポジトリを複製やフォークした際は、元のライセンス表記は省略したり削除してかまいません。

`tool/serve.go` は Apache License 2.0 で頒布されている github.com/hajimehoshi/wasmserve を eihigh が改変したものです。
`tool/serve.go` `tool/build.go` は Apache License 2.0 で頒布されている github.com/hajimehoshi/wasmserve を eihigh が改変したものです。
127 changes: 103 additions & 24 deletions tool/build.go
Original file line number Diff line number Diff line change
@@ -1,17 +1,44 @@
// This file is based on github.com/hajimehoshi/wasmserve and modified by eihigh.

// The original license notice:
//
// Copyright 2018 Hajime Hoshi
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package main

import (
"bytes"
"encoding/base64"
"errors"
"flag"
"fmt"
"io"
"io/fs"
"net/http"
"net/url"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
)

var (
tempDir = filepath.Join(os.TempDir(), "wasmgame")
errCachingFailed = errors.New("caching failed")
)

func build(args []string) error {
// Parse flags
flag := flag.NewFlagSet("build", flag.ExitOnError)
Expand All @@ -22,28 +49,16 @@ func build(args []string) error {
}

addr := flag.String("http", defaultAddr, "HTTP service address")
tags := flag.String("tags", "", "Build tags")
flag.Parse(args)

if flag.NArg() > 0 {
fmt.Fprintln(os.Stderr, "Unexpected arguments:", flag.Args())
fmt.Fprintln(os.Stderr, "unexpected arguments:", flag.Args())
flag.Usage()
}

// Copy $GOROOT/misc/wasm/wasm_exec.js
goroot := findGOROOT()
src := filepath.Join(goroot, "misc", "wasm", "wasm_exec.js")
dst := "wasm_exec.js"
if err := copyFile(dst, src); err != nil {
return fmt.Errorf("copy wasm_exec.js: %w", err)
}

// Run go build
cmd := exec.Command("go", "build", "-o", "game.wasm")
cmd.Env = append(os.Environ(), "GOOS=js", "GOARCH=wasm")
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
return fmt.Errorf("go build: %w", err)
if err := execBuild(*tags); err != nil {
return err
}

// After building, send a request to '_notify' to automatically reload the browser
Expand All @@ -59,14 +74,78 @@ func build(args []string) error {
return nil
}

func findGOROOT() string {
if env := os.Getenv("GOROOT"); env != "" {
return filepath.Clean(env)
func execBuild(tags string) error {
v, err := goVersion(".")
if err != nil {
return err
}

f, err := getCachedWasmExecJS(v)
if errors.Is(err, fs.ErrNotExist) {
f, err = fetchWasmExecJS(v)
if err != nil {
return err
}
} else if err != nil {
return err
}
def := filepath.Clean(runtime.GOROOT())
out, err := exec.Command("go", "env", "GOROOT").Output()

if err := os.WriteFile("wasm_exec.js", f, 0666); err != nil {
return err
}

cmd := exec.Command("go", "build", "-o", "game.wasm", "-tags", tags)
cmd.Env = append(os.Environ(), "GOOS=js", "GOARCH=wasm")
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
return fmt.Errorf("go build: %w", err)
}

return nil
}

// goVersion fetches the current using Go's version.
// goVersion is different from runtime.Version(), which returns a Go version for this wasmgame build.
func goVersion(builddir string) (string, error) {
cmd := exec.Command("go", "list", "-f", "go{{.Module.GoVersion}}", builddir)

var stderr bytes.Buffer
cmd.Stderr = io.MultiWriter(os.Stderr, &stderr)

out, err := cmd.Output()
if err != nil {
return def
return "", fmt.Errorf("%s%w", stderr.String(), err)
}
return strings.TrimSpace(string(out))

return strings.TrimSpace(string(out)), nil
}

func getCachedWasmExecJS(version string) ([]byte, error) {
cachefile := filepath.Join(tempDir, version, "misc", "wasm", "wasm_exec.js")
return os.ReadFile(cachefile)
}

func fetchWasmExecJS(version string) ([]byte, error) {
url := fmt.Sprintf("https://go.googlesource.com/go/+/refs/tags/%s/misc/wasm/wasm_exec.js?format=TEXT", version)
resp, err := http.Get(url)
if err != nil {
return nil, err
}
defer resp.Body.Close()

content, err := io.ReadAll(base64.NewDecoder(base64.StdEncoding, resp.Body))
if err != nil {
return nil, err
}

cachefile := filepath.Join(tempDir, version, "misc", "wasm", "wasm_exec.js")
if err := os.MkdirAll(filepath.Dir(cachefile), 0777); err != nil {
return content, errors.Join(err, errCachingFailed)
}
if err := os.WriteFile(cachefile, content, 0777); err != nil {
return content, errors.Join(err, errCachingFailed)
}

return content, nil
}
45 changes: 45 additions & 0 deletions tool/build_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package main

import (
"bytes"
"errors"
"io/fs"
"os"
"testing"
)

func TestFetchWasmExecJS(t *testing.T) {
if err := os.RemoveAll(tempDir); err != nil {
t.Fatal(err)
}
v, err := goVersion(".")
if err != nil {
t.Fatalf("get go version: %v", err)
}

cache, err := getCachedWasmExecJS(v)
if err == nil {
t.Fatal("cache should be missing")
}
if !errors.Is(err, fs.ErrNotExist) {
t.Fatalf("cache file has an error: %v", err)
}

src, err := fetchWasmExecJS(v)
if err != nil {
t.Fatalf("fetch from source: %v", err)
}
if len(src) == 0 {
t.Fatal("cannot fetch from source")
}
t.Log(string(src[:100]))

cache, err = getCachedWasmExecJS(v)
if err != nil {
t.Fatalf("cannot get cache: %v", err)
}
if !bytes.Equal(src, cache) {
t.Fatal("source != cache")
}
t.Log(string(cache[:100]))
}
80 changes: 34 additions & 46 deletions tool/dist.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package main

import (
"archive/zip"
"errors"
"flag"
"fmt"
"io"
Expand All @@ -12,26 +13,32 @@ import (
"strings"
)

const distRoot = "dist"
const outputDir = "dist"

// distFiles and distDirs are the files and directories to be included in the distribution.
// If you want to include more files or directories, add them to these lists.
var distFiles = []string{
"index.html",
"game.html",
"game.wasm",
"wasm_exec.js",
// "favicon.ico",
// ...
}

// Note that files that start with a dot are excluded even if they are under distDir.
// To include such files, add them to distFiles individually.
var distDirs = []string{
"asset",
}

// isDist reports whether the name is part of the distribution.
func isDist(name string) bool {
name = filepath.Clean(name)
name, _ = filepath.Abs(name)

// Return true if the name exactly matches distFiles.
for _, f := range distFiles {
f = filepath.Clean(f)
f, _ = filepath.Abs(f)
if name == f {
return true
}
Expand All @@ -45,7 +52,7 @@ func isDist(name string) bool {

// Return true if the file is under distDirs.
for _, d := range distDirs {
d = filepath.Clean(d)
d, _ = filepath.Abs(d)
if strings.HasPrefix(name, d) {
return true
}
Expand All @@ -64,41 +71,42 @@ func dist(args []string) error {
}

zips := flag.Bool("zip", false, "bundle the artifacts as dist.zip")
tags := flag.String("tags", "", "Build tags")
flag.Parse(args)

if flag.NArg() > 0 {
fmt.Fprintln(os.Stderr, "Unexpected arguments:", flag.Args())
fmt.Fprintln(os.Stderr, "unexpected arguments:", flag.Args())
flag.Usage()
}

// Build before copying
if err := build(nil); err != nil {
if err := execBuild(*tags); err != nil {
return fmt.Errorf("build: %w", err)
}

// Reset the existing distribution
if err := os.RemoveAll(distRoot); err != nil {
// Reset the existing output
if err := os.RemoveAll(outputDir); err != nil {
return fmt.Errorf("remove existing dist: %w", err)
}
if err := os.MkdirAll(distRoot, 0777); err != nil {
if err := os.MkdirAll(outputDir, 0777); err != nil {
return fmt.Errorf("ensure dist: %w", err)
}

// Copy files
// Copy distFiles
for _, f := range distFiles {
dst := filepath.Join(distRoot, f)
d := filepath.Dir(dst)
if err := os.MkdirAll(d, 0777); err != nil {
return fmt.Errorf("ensure dir for %s: %w", f, err)
}
dst := filepath.Join(outputDir, f)
if err := copyFile(dst, f); err != nil {
return fmt.Errorf("copy file %s: %w", f, err)
if errors.Is(err, fs.ErrNotExist) {
return fmt.Errorf("files listed in distFiles are required: %s", f)
} else {
return fmt.Errorf("copy file %s: %w", f, err)
}
}
}

// Copy directories recursively
for _, d := range distDirs {
dst := filepath.Join(distRoot, d)
dst := filepath.Join(outputDir, d)
if err := copyDir(dst, d); err != nil {
return fmt.Errorf("copy dir %s: %w", d, err)
}
Expand Down Expand Up @@ -127,8 +135,13 @@ func copyDir(dst, src string) error {
return nil
}

rel, err := filepath.Rel(src, name)
if err != nil {
return err
}
dst := filepath.Join(dst, rel)

// Ensure directory if the entry is a directory
dst := filepath.Join(distRoot, name)
if entry.IsDir() {
if err := os.MkdirAll(dst, 0777); err != nil {
return fmt.Errorf("ensure dir for %s: %w", name, err)
Expand Down Expand Up @@ -158,6 +171,7 @@ func copyFile(dst, src string) error {
if _, err := io.Copy(w, r); err != nil {
return err
}
// We ignore the permissions
return nil
}

Expand All @@ -169,32 +183,6 @@ func zipDist() error {
defer output.Close()

zw := zip.NewWriter(output)
defer zw.Close()

// Write contents of the distribution to 'dist.zip' recursively
return filepath.WalkDir(distRoot, func(name string, entry fs.DirEntry, err error) error {
if err != nil {
return fmt.Errorf("walk %s: %w", name, err)
}
if entry.IsDir() {
return nil
}

f, err := os.Open(name)
if err != nil {
return fmt.Errorf("open %s: %w", name, err)
}
defer f.Close()

rel, _ := filepath.Rel(distRoot, name)
w, err := zw.Create(rel)
if err != nil {
return fmt.Errorf("create %s as %s in zip: %w", name, rel, err)
}

if _, err = io.Copy(w, f); err != nil {
return fmt.Errorf("copy %s as %s into zip: %w", name, rel, err)
}
return nil
})
zw.AddFS(os.DirFS(outputDir))
return zw.Close()
}
Loading

0 comments on commit 0a2bec9

Please sign in to comment.