Skip to content

Commit

Permalink
fix #1399: avoid "os.MkdirAll" to fix WebAssembly
Browse files Browse the repository at this point in the history
  • Loading branch information
evanw committed Jun 28, 2021
1 parent 97416e7 commit b94802a
Show file tree
Hide file tree
Showing 5 changed files with 96 additions and 4 deletions.
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
# Changelog

## Unreleased

* Fix nested output directories with WebAssembly on Windows ([#1399](https://github.com/evanw/esbuild/issues/1399))

Many functions in Go's standard library have a bug where they do not work on Windows when using Go with WebAssembly. This is a long-standing bug and is a fault with the design of the standard library, so it's unlikely to be fixed. Basically Go's standard library is designed to bake "Windows or not" decision into the compiled executable, but WebAssembly is platform-independent which makes "Windows or not" is a run-time decision instead of a compile-time decision. Oops.

I have been working around this by trying to avoid using path-related functions in the Go standard library and doing all path manipulation by myself instead. This involved completely replacing Go's `path/filepath` library. However, I missed the `os.MkdirAll` function which is also does path manipulation but is outside of the `path/filepath` package. This meant that nested output directories failed to be created on Windows, which caused a build error. This problem only affected the `esbuild-wasm` package.

This release manually reimplements nested output directory creation to work around this bug in the Go standard library. So nested output directories should now work on Windows with the `esbuild-wasm` package.

## 0.12.10

* Add a target for ES2021
Expand Down
39 changes: 39 additions & 0 deletions internal/fs/fs.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@ package fs

import (
"errors"
"os"
"strings"
"sync"
"syscall"
)

type EntryKind uint8
Expand Down Expand Up @@ -160,3 +162,40 @@ func BeforeFileOpen() {
func AfterFileClose() {
<-fileOpenLimit
}

// This is a fork of "os.MkdirAll" to work around bugs with the WebAssembly
// build target. More information here: https://github.com/golang/go/issues/43768.
func MkdirAll(fs FS, path string, perm os.FileMode) error {
// Run "Join" once to run "Clean" on the path, which removes trailing slashes
return mkdirAll(fs, fs.Join(path), perm)
}

func mkdirAll(fs FS, path string, perm os.FileMode) error {
// Fast path: if we can tell whether path is a directory or file, stop with success or error.
if dir, err := os.Stat(path); err == nil {
if dir.IsDir() {
return nil
}
return &os.PathError{Op: "mkdir", Path: path, Err: syscall.ENOTDIR}
}

// Slow path: make sure parent exists and then call Mkdir for path.
if parent := fs.Dir(path); parent != path {
// Create parent.
if err := mkdirAll(fs, parent, perm); err != nil {
return err
}
}

// Parent now exists; invoke Mkdir and use its result.
if err := os.Mkdir(path, perm); err != nil {
// Handle arguments like "foo/." by
// double-checking that directory doesn't exist.
dir, err1 := os.Lstat(path)
if err1 == nil && dir.IsDir() {
return nil
}
return err
}
return nil
}
2 changes: 1 addition & 1 deletion pkg/api/api_impl.go
Original file line number Diff line number Diff line change
Expand Up @@ -989,7 +989,7 @@ func rebuildImpl(
go func(result graph.OutputFile) {
fs.BeforeFileOpen()
defer fs.AfterFileClose()
if err := os.MkdirAll(realFS.Dir(result.AbsPath), 0755); err != nil {
if err := fs.MkdirAll(realFS, realFS.Dir(result.AbsPath), 0755); err != nil {
log.AddError(nil, logger.Loc{}, fmt.Sprintf(
"Failed to create output directory: %s", err.Error()))
} else {
Expand Down
7 changes: 4 additions & 3 deletions pkg/cli/cli_impl.go
Original file line number Diff line number Diff line change
Expand Up @@ -703,7 +703,8 @@ func runImpl(osArgs []string) int {
logger.PrintErrorToStderr(osArgs, "Cannot use \"metafile\" without an output path")
return 1
}
if realFS, err := fs.RealFS(fs.RealFSOptions{AbsWorkingDir: buildOptions.AbsWorkingDir}); err == nil {
realFS, realFSErr := fs.RealFS(fs.RealFSOptions{AbsWorkingDir: buildOptions.AbsWorkingDir})
if realFSErr == nil {
absPath, ok := realFS.Abs(*metafile)
if !ok {
logger.PrintErrorToStderr(osArgs, fmt.Sprintf("Invalid metafile path: %s", *metafile))
Expand All @@ -716,7 +717,7 @@ func runImpl(osArgs []string) int {
}

writeMetafile = func(json string) {
if json == "" {
if json == "" || realFSErr != nil {
return // Don't write out the metafile on build errors
}
if err != nil {
Expand All @@ -725,7 +726,7 @@ func runImpl(osArgs []string) int {
}
fs.BeforeFileOpen()
defer fs.AfterFileClose()
if err := os.MkdirAll(metafileAbsDir, 0755); err != nil {
if err := fs.MkdirAll(realFS, metafileAbsDir, 0755); err != nil {
logger.PrintErrorToStderr(osArgs, fmt.Sprintf(
"Failed to create output directory: %s", err.Error()))
} else {
Expand Down
42 changes: 42 additions & 0 deletions scripts/wasm-tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,48 @@ const tests = {
assert.deepStrictEqual(jsNative, jsWASM);
},

outfileNestedTest({ testDir, esbuildPathWASM }) {
const outfile = path.join(__dirname, 'a', 'b', 'c', 'd', 'out.js');
child_process.execFileSync('node', [
esbuildPathWASM,
'--bundle',
'--format=cjs',
'--outfile=' + outfile,
'--log-level=warning',
], {
stdio: ['pipe', 'pipe', 'inherit'],
cwd: testDir,
input: `export default 123`,
}).toString();

// Check that the bundle is valid
const exports = require(outfile);
assert.deepStrictEqual(exports.default, 123);
},

metafileNestedTest({ testDir, esbuildPathWASM }) {
const outfile = path.join(__dirname, 'out.js');
const metafile = path.join(__dirname, 'a', 'b', 'c', 'd', 'meta.json');
child_process.execFileSync('node', [
esbuildPathWASM,
'--bundle',
'--format=cjs',
'--outfile=' + outfile,
'--metafile=' + metafile,
'--log-level=warning',
], {
stdio: ['pipe', 'pipe', 'inherit'],
cwd: testDir,
input: `export default 123`,
}).toString();

// Check that the bundle is valid
const exports = require(outfile);
assert.deepStrictEqual(exports.default, 123);
const json = JSON.parse(fs.readFileSync(metafile, 'utf8'));
assert.deepStrictEqual(json.outputs['../../out.js'].entryPoint, '<stdin>');
},

importRelativeFileTest({ testDir, esbuildPathWASM }) {
const outfile = path.join(testDir, 'out.js')
const packageJSON = path.join(__dirname, '..', 'npm', 'esbuild-wasm', 'package.json');
Expand Down

0 comments on commit b94802a

Please sign in to comment.