fix: devserver not working on windows
refactor: remove unnecessary runfile stat
Closes #385
PiperOrigin-RevId: 233100030
diff --git a/.bazelci/presubmit.yml b/.bazelci/presubmit.yml
index f587cc9..5b60a2c 100644
--- a/.bazelci/presubmit.yml
+++ b/.bazelci/presubmit.yml
@@ -52,6 +52,4 @@
- "--action_env=PATH"
- "--test_env=PATH"
test_targets:
- - "--"
- "..."
- - "-//devserver/devserver:go_default_test"
diff --git a/devserver/devserver/devserver.go b/devserver/devserver/devserver.go
index 4748908..9bd3a86 100644
--- a/devserver/devserver/devserver.go
+++ b/devserver/devserver/devserver.go
@@ -97,13 +97,7 @@
func CreateFileHandler(servingPath, manifest string, pkgs []string, base string) http.HandlerFunc {
// We want to add the root runfile path because by default developers should be able to request
// runfiles through their absolute manifest path (e.g. "my_workspace_name/src/file.css")
- // We use the empty string package because of the different algorithm for
- // file resolution used internally and externally.
- pkgPaths := dirHTTPFileSystem{[]string{"./"}, base}
- for _, pkg := range pkgs {
- pkgPaths.files = append(pkgPaths.files, pathReplacer.Replace(pkg))
- }
- pkgPaths.files = append(pkgPaths.files, base)
+ pkgPaths := dirHTTPFileSystem{append(pkgs, "./"), base}
fileHandler := http.FileServer(pkgPaths).ServeHTTP
@@ -127,8 +121,8 @@
userIndexFile, err := runfiles.Runfile(base, pathReplacer.Replace(filepath.Join(pkg, "index.html")))
// In case the potential user index file couldn't be found in the runfiles,
- // just continue searching.
- if _, statErr := os.Stat(userIndexFile); err != nil || statErr != nil {
+ // continue searching within other packages.
+ if err != nil {
continue
}
@@ -175,34 +169,40 @@
// dirHTTPFileSystem implements http.FileSystem by looking in the list of dirs one after each other.
type dirHTTPFileSystem struct {
- files []string
+ packageDirs []string
base string
}
func (fs dirHTTPFileSystem) Open(name string) (http.File, error) {
- for _, packageName := range fs.files {
- filePackageName := filepath.Join(packageName, name)
- realFilePath, err := runfiles.Runfile(fs.base, filePackageName)
- stat, statErr := os.Stat(realFilePath)
+ for _, packageName := range fs.packageDirs {
+ manifestFilePath := filepath.Join(packageName, name)
+ realFilePath, err := runfiles.Runfile(fs.base, manifestFilePath)
- if err != nil || statErr != nil {
+ if err != nil {
// In case the runfile could not be found, we also need to check that the requested
// path does not refer to a directory containing an "index.html" file. This can
// happen if Bazel runs without runfile symlinks, where only files can be resolved
// from the manifest. In that case we dirty check if there is a "index.html" file.
- realFilePath, err = runfiles.Runfile(fs.base, filepath.Join(filePackageName, "index.html"))
+ realFilePath, err = runfiles.Runfile(fs.base, filepath.Join(manifestFilePath, "index.html"))
- // Continue searching if the runfile couldn't be found for the request filed.
- if _, statErr := os.Stat(realFilePath); err != nil || statErr != nil {
+ // Continue searching if the runfile couldn't be found for the requested file.
+ if err != nil {
continue
}
}
+ stat, err := os.Stat(realFilePath)
+ if err != nil {
+ // This should actually never happen because runfiles resolved through the runfile helpers
+ // should always exist. Just in order to properly handle the error, we add this error handling.
+ return nil, fmt.Errorf("could not read runfile %s", manifestFilePath)
+ }
+
// In case the resolved file resolves to a directory. This can only happen if
// Bazel runs with symlinked runfiles (e.g. on MacOS, linux). In that case, we
// just look for a index.html in the directory.
if stat.IsDir() {
- realFilePath, err = runfiles.Runfile(fs.base, filepath.Join(filePackageName, "index.html"))
+ realFilePath, err = runfiles.Runfile(fs.base, filepath.Join(manifestFilePath, "index.html"))
// In case the index.html file of the requested directory couldn't be found,
// we just continue searching.
diff --git a/devserver/main.go b/devserver/main.go
index 29c224b..7b0a2a0 100644
--- a/devserver/main.go
+++ b/devserver/main.go
@@ -8,15 +8,17 @@
"io/ioutil"
"net/http"
"os"
- "path/filepath"
"strings"
"github.com/bazelbuild/rules_typescript/devserver/concatjs"
"github.com/bazelbuild/rules_typescript/devserver/devserver"
+ "github.com/bazelbuild/rules_typescript/devserver/runfiles"
)
var (
port = flag.Int("port", 5432, "server port to listen on")
+ // The "base" CLI flag is only kept because within Google3 because removing would be a breaking change due to
+ // ConcatJS and "devserver/devserver.go" still respecting the specified base flag.
base = flag.String("base", "", "server base (required, runfiles of the binary)")
pkgs = flag.String("packages", "", "root package(s) to serve, comma-separated")
manifest = flag.String("manifest", "", "sources manifest (.MF)")
@@ -28,23 +30,30 @@
func main() {
flag.Parse()
- if *base == "" || len(*pkgs) == 0 || (*manifest == "") || (*scriptsManifest == "") {
+ if len(*pkgs) == 0 || (*manifest == "") || (*scriptsManifest == "") {
fmt.Fprintf(os.Stderr, "Required argument not set\n")
os.Exit(1)
}
- if _, err := os.Stat(*base); err != nil {
- fmt.Fprintf(os.Stderr, "Cannot read server base %s: %v\n", *base, err)
+ manifestPath, err := runfiles.Runfile(*base, *scriptsManifest)
+
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "Failed to find scripts_manifest in runfiles: %v\n", err)
os.Exit(1)
}
- scriptsManifestPath := filepath.Join(*base, *scriptsManifest)
- scriptFiles, err := manifestFiles(scriptsManifestPath)
+ scriptFiles, err := manifestFiles(manifestPath)
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to read scripts_manifest: %v\n", err)
os.Exit(1)
}
+ if !strings.HasPrefix(*servingPath, "/") {
+ fmt.Fprintf(os.Stderr, "The specified serving_path does not start with a slash. "+
+ "This causes the serving path to not have any effect.\n")
+ os.Exit(1)
+ }
+
preScripts := make([]string, 0, 100)
postScripts := make([]string, 0, 1)
@@ -76,7 +85,12 @@
// the requirejs script which is added to scriptFiles by the devserver
// skylark rule.
for _, v := range scriptFiles {
- js, err := loadScript(filepath.Join(*base, v))
+ runfile, err := runfiles.Runfile(*base, v)
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "Could not find runfile %s, got error %s", v, err)
+ }
+
+ js, err := loadScript(runfile)
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to read script %s: %v\n", v, err)
} else {
@@ -90,7 +104,8 @@
postScripts = append(postScripts, fmt.Sprintf("require([\"%s\"]);", *entryModule))
}
- http.Handle(*servingPath, concatjs.ServeConcatenatedJS(*manifest, *base, preScripts, postScripts, nil /* realFileSystem */))
+ http.Handle(*servingPath, concatjs.ServeConcatenatedJS(*manifest, *base, preScripts, postScripts,
+ &RunfileFileSystem{}))
pkgList := strings.Split(*pkgs, ",")
http.HandleFunc("/", devserver.CreateFileHandler(*servingPath, *manifest, pkgList, *base))
diff --git a/devserver/runfile-filesystem.go b/devserver/runfile-filesystem.go
index 3f99c2f..090ded6 100644
--- a/devserver/runfile-filesystem.go
+++ b/devserver/runfile-filesystem.go
@@ -30,6 +30,6 @@
// ResolvePath resolves the specified path within a given root using Bazel's runfile resolution.
// This is necessary because on Windows, runfiles are not symlinked and need to be
// resolved using the runfile manifest file.
-func (fs *RunfileFileSystem) ResolvePath(root string, file string) (string, error) {
- return runfiles.Runfile(root, file)
+func (fs *RunfileFileSystem) ResolvePath(root string, manifestFilePath string) (string, error) {
+ return runfiles.Runfile(root, manifestFilePath)
}
diff --git a/devserver/runfiles/runfiles.go b/devserver/runfiles/runfiles.go
index 1d03448..cff9acc 100644
--- a/devserver/runfiles/runfiles.go
+++ b/devserver/runfiles/runfiles.go
@@ -4,7 +4,7 @@
import "github.com/bazelbuild/rules_go/go/tools/bazel"
// Runfile returns the base directory to the bazel runfiles
-func Runfile(_ string, path string) (string, error) {
- return bazel.Runfile(path)
+func Runfile(_base string, manifestPath string) (string, error) {
+ return bazel.Runfile(manifestPath)
}
diff --git a/internal/devserver/BUILD b/internal/devserver/BUILD
index d6e9733..2dfa86b 100644
--- a/internal/devserver/BUILD
+++ b/internal/devserver/BUILD
@@ -19,6 +19,7 @@
])
exports_files([
+ "launcher_template.sh",
# Exported to be consumed for generating skydoc.
"ts_devserver.bzl",
])
diff --git a/internal/devserver/launcher_template.sh b/internal/devserver/launcher_template.sh
new file mode 100644
index 0000000..13722a9
--- /dev/null
+++ b/internal/devserver/launcher_template.sh
@@ -0,0 +1,59 @@
+#!/bin/bash
+
+# Copyright 2018 The Bazel Authors. All rights reserved.
+#
+# 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.
+
+set -e
+
+# --- begin runfiles.bash initialization ---
+# Source the runfiles library:
+# https://github.com/bazelbuild/bazel/blob/master/tools/bash/runfiles/runfiles.bash
+# The runfiles library defines rlocation, which is a platform independent function
+# used to lookup the runfiles locations. This code snippet is needed at the top
+# of scripts that use rlocation to lookup the location of runfiles.bash and source it
+if [[ ! -d "${RUNFILES_DIR:-/dev/null}" && ! -f "${RUNFILES_MANIFEST_FILE:-/dev/null}" ]]; then
+ if [[ -f "$0.runfiles_manifest" ]]; then
+ export RUNFILES_MANIFEST_FILE="$0.runfiles_manifest"
+ elif [[ -f "$0.runfiles/MANIFEST" ]]; then
+ export RUNFILES_MANIFEST_FILE="$0.runfiles/MANIFEST"
+ elif [[ -f "$0.runfiles/bazel_tools/tools/bash/runfiles/runfiles.bash" ]]; then
+ export RUNFILES_DIR="$0.runfiles"
+ fi
+fi
+if [[ -f "${RUNFILES_DIR:-/dev/null}/bazel_tools/tools/bash/runfiles/runfiles.bash" ]]; then
+ source "${RUNFILES_DIR}/bazel_tools/tools/bash/runfiles/runfiles.bash"
+elif [[ -f "${RUNFILES_MANIFEST_FILE:-/dev/null}" ]]; then
+ source "$(grep -m1 "^bazel_tools/tools/bash/runfiles/runfiles.bash " \
+ "$RUNFILES_MANIFEST_FILE" | cut -d ' ' -f 2-)"
+else
+ echo >&2 "ERROR: cannot find @bazel_tools//tools/bash/runfiles:runfiles.bash"
+ exit 1
+fi
+# --- end runfiles.bash initialization ---
+
+
+readonly main=$(rlocation "TEMPLATED_main")
+readonly manifest=$(rlocation "TEMPLATED_manifest")
+readonly scripts_manifest=$(rlocation "TEMPLATED_scripts_manifest")
+
+# Workaround for https://github.com/bazelbuild/bazel/issues/6764
+# If this issue is incorporated into Bazel, the workaround here should be removed.
+MSYS2_ARG_CONV_EXCL="*" "${main}" \
+ -packages=TEMPLATED_packages \
+ -serving_path=TEMPLATED_serving_path \
+ -entry_module=TEMPLATED_entry_module \
+ -port=TEMPLATED_port \
+ -manifest="${manifest}" \
+ -scripts_manifest="${scripts_manifest}" \
+ "$@"
diff --git a/internal/devserver/ts_devserver.bzl b/internal/devserver/ts_devserver.bzl
index 3fd9519..fe377ec 100644
--- a/internal/devserver/ts_devserver.bzl
+++ b/internal/devserver/ts_devserver.bzl
@@ -24,6 +24,14 @@
"html_asset_inject",
)
+# Helper function to convert a short path to a path that is
+# found in the MANIFEST file.
+def _short_path_to_manifest_path(ctx, short_path):
+ if short_path.startswith("../"):
+ return short_path[3:]
+ else:
+ return ctx.workspace_name + "/" + short_path
+
def _ts_devserver(ctx):
files = depset()
for d in ctx.attr.deps:
@@ -52,7 +60,7 @@
amd_names_shim = ctx.actions.declare_file(
"_%s.amd_names_shim.js" % ctx.label.name,
- sibling = ctx.outputs.executable,
+ sibling = ctx.outputs.script,
)
write_amd_names_shim(ctx.actions, amd_names_shim, ctx.attr.bootstrap)
@@ -76,6 +84,7 @@
]
devserver_runfiles += ctx.files.static_files
devserver_runfiles += script_files
+ devserver_runfiles += ctx.files._bash_runfile_helpers
if ctx.file.index_html:
injected_index = ctx.actions.declare_file("index.html")
@@ -96,37 +105,24 @@
)
devserver_runfiles += [injected_index]
- serving_arg = ""
- if ctx.attr.serving_path:
- serving_arg = "-serving_path=%s" % ctx.attr.serving_path
-
packages = depset(["/".join([workspace_name, ctx.label.package])] + ctx.attr.additional_root_paths)
- # FIXME: more bash dependencies makes Windows support harder
- ctx.actions.write(
- output = ctx.outputs.executable,
+ ctx.actions.expand_template(
+ template = ctx.file._launcher_template,
+ output = ctx.outputs.script,
+ substitutions = {
+ "TEMPLATED_entry_module": ctx.attr.entry_module,
+ "TEMPLATED_main": _short_path_to_manifest_path(ctx, ctx.executable._devserver.short_path),
+ "TEMPLATED_manifest": _short_path_to_manifest_path(ctx, ctx.outputs.manifest.short_path),
+ "TEMPLATED_packages": ",".join(packages.to_list()),
+ "TEMPLATED_port": str(ctx.attr.port),
+ "TEMPLATED_scripts_manifest": _short_path_to_manifest_path(ctx, ctx.outputs.scripts_manifest.short_path),
+ "TEMPLATED_serving_path": ctx.attr.serving_path if ctx.attr.serving_path else "",
+ "TEMPLATED_workspace": workspace_name,
+ },
is_executable = True,
- content = """#!/bin/sh
-RUNFILES="$PWD/.."
-{main} {serving_arg} \
- -base="$RUNFILES" \
- -packages={packages} \
- -manifest={workspace}/{manifest} \
- -scripts_manifest={workspace}/{scripts_manifest} \
- -entry_module={entry_module} \
- -port={port} \
- "$@"
-""".format(
- main = ctx.executable._devserver.short_path,
- serving_arg = serving_arg,
- workspace = workspace_name,
- packages = ",".join(packages.to_list()),
- manifest = ctx.outputs.manifest.short_path,
- scripts_manifest = ctx.outputs.scripts_manifest.short_path,
- entry_module = ctx.attr.entry_module,
- port = str(ctx.attr.port),
- ),
)
+
return [DefaultInfo(
runfiles = ctx.runfiles(
files = devserver_runfiles,
@@ -191,6 +187,7 @@
allow_files = True,
aspects = [sources_aspect],
),
+ "_bash_runfile_helpers": attr.label(default = Label("@bazel_tools//tools/bash/runfiles")),
"_devserver": attr.label(
# For local development in rules_typescript, we build the devserver from sources.
# This requires that we have the go toolchain available.
@@ -206,33 +203,52 @@
executable = True,
cfg = "host",
),
+ "_launcher_template": attr.label(allow_single_file = True, default = Label("//internal/devserver:launcher_template.sh")),
"_requirejs_script": attr.label(allow_single_file = True, default = Label("@build_bazel_rules_typescript_devserver_deps//node_modules/requirejs:require.js")),
},
outputs = {
"manifest": "%{name}.MF",
+ "script": "%{name}.sh",
"scripts_manifest": "scripts_%{name}.MF",
},
- executable = True,
)
"""ts_devserver is a simple development server intended for a quick "getting started" experience.
Additional documentation at https://github.com/alexeagle/angular-bazel-example/wiki/Running-a-devserver-under-Bazel
"""
-def ts_devserver_macro(tags = [], **kwargs):
- """ibazel wrapper for `ts_devserver`
+def ts_devserver_macro(name, data = [], args = [], visibility = None, tags = [], testonly = 0, **kwargs):
+ """Macro for creating a `ts_devserver`
- This macro re-exposes the `ts_devserver` rule with some extra tags so that
- it behaves correctly under ibazel.
-
+ This macro re-exposes a `sh_binary` and `ts_devserver` target that can run the
+ actual devserver implementation.
+ The `ts_devserver` rule is just responsible for generating a launcher script
+ that runs the Go devserver implementation. The `sh_binary` is the primary
+ target that matches the specified "name" and executes the generated bash
+ launcher script.
This is re-exported in `//:defs.bzl` as `ts_devserver` so if you load the rule
from there, you actually get this macro.
-
Args:
- tags: standard Bazel tags, this macro adds a couple for ibazel
+ name: Name of the devserver target
+ data: Runtime dependencies for the devserver
+ args: Command line arguments that will be passed to the devserver Go implementation
+ visibility: Visibility of the devserver targets
+ tags: Standard Bazel tags, this macro adds a couple for ibazel
+ testonly: Whether the devserver should only run in `bazel test`
**kwargs: passed through to `ts_devserver`
"""
ts_devserver(
+ name = "%s_launcher" % name,
+ data = data + ["@bazel_tools//tools/bash/runfiles"],
+ testonly = testonly,
+ visibility = ["//visibility:private"],
+ tags = tags,
+ **kwargs
+ )
+
+ native.sh_binary(
+ name = name,
+ args = args,
# Users don't need to know that these tags are required to run under ibazel
tags = tags + [
# Tell ibazel not to restart the devserver when its deps change.
@@ -241,5 +257,8 @@
# this program.
"ibazel_live_reload",
],
- **kwargs
+ srcs = ["%s_launcher.sh" % name],
+ data = [":%s_launcher" % name],
+ testonly = testonly,
+ visibility = visibility,
)