diff --git a/.bazelci/presubmit.yml b/.bazelci/presubmit.yml index 03be674..0d28519 100644 --- a/.bazelci/presubmit.yml +++ b/.bazelci/presubmit.yml @@ -11,6 +11,11 @@ tasks: - "//..." test_targets: - "//..." + windows: + build_targets: + - "//..." + test_targets: + - "//..." e2e_ubuntu2204: platform: ubuntu2204 diff --git a/.bazelrc b/.bazelrc new file mode 100644 index 0000000..265d4e1 --- /dev/null +++ b/.bazelrc @@ -0,0 +1,25 @@ +############################################################################### +## Bazel Configuration Flags +## +## `.bazelrc` is a Bazel configuration file. +## https://bazel.build/docs/best-practices#bazelrc-file +############################################################################### + +# https://github.com/bazelbuild/bazel/issues/8195 +build --incompatible_disallow_empty_glob=true + +# https://github.com/bazelbuild/bazel/issues/12821 +build --nolegacy_external_runfiles + +# https://github.com/bazelbuild/bazel/issues/23043. +build --incompatible_autoload_externally= + +############################################################################### +## Custom user flags +## +## This should always be the last thing in the `.bazelrc` file to ensure +## consistent behavior when setting flags in that file as `.bazelrc` files are +## evaluated top to bottom. +############################################################################### + +try-import %workspace%/user.bazelrc diff --git a/.gitignore b/.gitignore index d97b24c..43b6234 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ /e2e/*/bazel-* .idea *.sw* +user.bazelrc diff --git a/MODULE.bazel b/MODULE.bazel index e843949..ba56ce8 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -10,6 +10,7 @@ module( bazel_dep(name = "platforms", version = "0.0.10") bazel_dep(name = "bazel_skylib", version = "1.7.1") bazel_dep(name = "rules_cc", version = "0.0.17") +bazel_dep(name = "rules_shell", version = "0.4.0") repos = use_extension("@rules_perl//perl:extensions.bzl", "perl_repositories") use_repo( @@ -35,5 +36,3 @@ use_repo( "fcgi", "genhtml", ) - -bazel_dep(name = "rules_shell", version = "0.4.0", dev_dependency = True) diff --git a/examples/genhtml/BUILD b/examples/genhtml/BUILD index 5f3e221..5535748 100644 --- a/examples/genhtml/BUILD +++ b/examples/genhtml/BUILD @@ -14,16 +14,33 @@ load("@rules_perl//perl:perl.bzl", "perl_test") +genrule( + name = "generated_html", + srcs = [ + "coverage.dat", + "genhtml_test.t", + ], + outs = ["index.html"], + cmd = "$(execpath @genhtml//:genhtml_bin) --quiet --output-directory $$(dirname $(execpath index.html)) $(execpath coverage.dat)", + tools = [ + "@genhtml//:genhtml_bin", + ], +) + perl_test( name = "genhtml_test", srcs = ["genhtml_test.t"], data = [ - "coverage.dat", - "genhtml_test.t", - "@genhtml//:genhtml_bin", + ":index.html", ], env = { - "GENHTML_BIN": "$(rlocationpath @genhtml//:genhtml_bin)", + "COVERAGE_INDEX_HTML": "$(rootpath :index.html)", }, + # TODO: A runfiles API should be implemented to find the `index.html` file for this test. + # For more details see: https://github.com/bazel-contrib/rules_perl/issues/85 + target_compatible_with = select({ + "@platforms//os:windows": ["@platforms//:incompatible"], + "//conditions:default": [], + }), visibility = ["//visibility:public"], ) diff --git a/examples/genhtml/genhtml_test.t b/examples/genhtml/genhtml_test.t index 64f67fd..5e76d01 100644 --- a/examples/genhtml/genhtml_test.t +++ b/examples/genhtml/genhtml_test.t @@ -14,8 +14,17 @@ use strict; use warnings; +use Test::More; -use Test::More tests => 1; +my $file = $ENV{'COVERAGE_INDEX_HTML'}; +ok(defined $file, 'COVERAGE_INDEX_HTML is set'); -`../$ENV{GENHTML_BIN} -o $ENV{TEST_UNDECLARED_OUTPUTS_DIR} examples/genhtml/coverage.dat`; -ok(-e "$ENV{TEST_UNDECLARED_OUTPUTS_DIR}/index.html", 'genhtml generated index.html'); +if (defined $file) { + open my $fh, '<', $file or die "Could not open file '$file': $!\n"; + my $content = do { local $/; <$fh> }; + close $fh; + + like($content, qr{LCOV - coverage\.dat}, 'Expected tag found'); +} + +done_testing; diff --git a/examples/hello_world/BUILD b/examples/hello_world/BUILD index 15cea2e..d13e77b 100644 --- a/examples/hello_world/BUILD +++ b/examples/hello_world/BUILD @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -load("//perl:perl.bzl", "perl_binary") +load("//perl:perl.bzl", "perl_binary", "perl_test") package(default_visibility = ["//visibility:public"]) @@ -20,3 +20,8 @@ perl_binary( name = "hello_world", srcs = ["hello_world.pl"], ) + +perl_test( + name = "hello_world_test", + srcs = ["hello_world_test.t"], +) diff --git a/examples/hello_world/hello_world_test.t b/examples/hello_world/hello_world_test.t new file mode 100644 index 0000000..29292e7 --- /dev/null +++ b/examples/hello_world/hello_world_test.t @@ -0,0 +1,5 @@ +use strict; +use warnings; +use Test::More tests => 1; + +is(1 + 2, 3, '1 + 2 equals 3'); diff --git a/perl/BUILD b/perl/BUILD index 63b6ae7..62c085f 100644 --- a/perl/BUILD +++ b/perl/BUILD @@ -1,7 +1,11 @@ load("//:platforms.bzl", "platforms") load(":toolchain.bzl", "current_perl_toolchain", "perl_toolchain") -exports_files(["binary_wrapper.tpl"]) +alias( + name = "binary_wrapper.tpl", + actual = "//perl/private:binary_wrapper.tpl", + visibility = ["//visibility:public"], +) # toolchain_type defines a name for a kind of toolchain. Our toolchains # declare that they have this type. Our rules request a toolchain of this type. diff --git a/perl/deps.bzl b/perl/deps.bzl index cd5dd68..d47be7a 100644 --- a/perl/deps.bzl +++ b/perl/deps.bzl @@ -48,6 +48,14 @@ def perl_rules_dependencies(): sha256 = "97e70364e9249702246c0e9444bccdc4b847bed1eb03c5a3ece4f83dfe6abc44", ) + _maybe( + http_archive, + name = "rules_shell", + sha256 = "3e114424a5c7e4fd43e0133cc6ecdfe54e45ae8affa14fadd839f29901424043", + strip_prefix = "rules_shell-0.4.0", + url = "https://github.com/bazelbuild/rules_shell/releases/download/v0.4.0/rules_shell-v0.4.0.tar.gz", + ) + def _maybe(rule, name, **kwargs): """Declares an external repository if it hasn't been declared already.""" if name not in native.existing_rules(): diff --git a/perl/private/BUILD b/perl/private/BUILD index 8ab63db..e55528e 100644 --- a/perl/private/BUILD +++ b/perl/private/BUILD @@ -1 +1,14 @@ -exports_files(["binary_wrapper.tpl"]) +exports_files([ + "binary_wrapper.bat.tpl", + "binary_wrapper.sh.tpl", + "entrypoint.pl", +]) + +alias( + name = "binary_wrapper.tpl", + actual = select({ + "@platforms//os:windows": "binary_wrapper.bat.tpl", + "//conditions:default": "binary_wrapper.sh.tpl", + }), + visibility = ["//visibility:public"], +) diff --git a/perl/private/binary_wrapper.bat.tpl b/perl/private/binary_wrapper.bat.tpl new file mode 100644 index 0000000..e2a000e --- /dev/null +++ b/perl/private/binary_wrapper.bat.tpl @@ -0,0 +1,76 @@ +@ECHO OFF + +SETLOCAL ENABLEEXTENSIONS +SETLOCAL ENABLEDELAYEDEXPANSION + +@REM Usage of rlocation function: +@REM +@REM call :rlocation <runfile_path> <abs_path> +@REM +@REM The rlocation function maps the given <runfile_path> to its absolute +@REM path and stores the result in a variable named <abs_path>. This +@REM function fails if the <runfile_path> doesn't exist in mainifest file. +:: Start of rlocation +goto :rlocation_end +:rlocation +if "%~2" equ "" ( + echo>&2 ERROR: Expected two arguments for rlocation function. + exit 1 +) +if exist "%RUNFILES_DIR%" ( + set RUNFILES_MANIFEST_FILE=%RUNFILES_DIR%_manifest +) +if "%RUNFILES_MANIFEST_FILE%" equ "" ( + set RUNFILES_MANIFEST_FILE=%~f0.runfiles\MANIFEST +) +if not exist "%RUNFILES_MANIFEST_FILE%" ( + set RUNFILES_MANIFEST_FILE=%~f0.runfiles_manifest +) +set MF=%RUNFILES_MANIFEST_FILE:/=\% +if not exist "%MF%" ( + echo>&2 ERROR: Manifest file %MF% does not exist. + exit 1 +) +set runfile_path=%~1 +for /F "tokens=2* usebackq" %%i in (`%SYSTEMROOT%\system32\findstr.exe /l /c:"!runfile_path! " "%MF%"`) do ( + set abs_path=%%i +) +if "!abs_path!" equ "" ( + echo>&2 ERROR: !runfile_path! not found in runfiles manifest + exit 1 +) +set %~2=!abs_path! +exit /b 0 +:rlocation_end + + +@REM Function to replace forward slashes with backslashes. +goto :slocation_end +:slocation +set "input=%~1" +set "varName=%~2" +set "output=" + +@REM Replace forward slashes with backslashes +set "output=%input:/=\%" + +@REM Assign the sanitized path to the specified variable +set "%varName%=%output%" +exit /b 0 +:slocation_end + + +call :rlocation "{interpreter}" INTERPRETER +call :rlocation "{entrypoint}" ENTRYPOINT +call :rlocation "{config}" CONFIG +call :rlocation "{main}" MAIN + +@REM Unset runfiles dir so windows consistently works with and without it. +set RUNFILES_DIR= + +%INTERPRETER% ^ + %ENTRYPOINT% ^ + %CONFIG% ^ + %MAIN% ^ + "--" ^ + %* diff --git a/perl/private/binary_wrapper.sh.tpl b/perl/private/binary_wrapper.sh.tpl new file mode 100644 index 0000000..61f0f76 --- /dev/null +++ b/perl/private/binary_wrapper.sh.tpl @@ -0,0 +1,30 @@ +#!/usr/bin/env bash + +# --- begin runfiles.bash initialization v3 --- +# Copy-pasted from the Bazel Bash runfiles library v3. +set -uo pipefail; set +e; f=bazel_tools/tools/bash/runfiles/runfiles.bash +# shellcheck disable=SC1090 +source "${RUNFILES_DIR:-/dev/null}/$f" 2>/dev/null || \ + source "$(grep -sm1 "^$f " "${RUNFILES_MANIFEST_FILE:-/dev/null}" | cut -f2- -d' ')" 2>/dev/null || \ + source "$0.runfiles/$f" 2>/dev/null || \ + source "$(grep -sm1 "^$f " "$0.runfiles_manifest" | cut -f2- -d' ')" 2>/dev/null || \ + source "$(grep -sm1 "^$f " "$0.exe.runfiles_manifest" | cut -f2- -d' ')" 2>/dev/null || \ + { echo>&2 "ERROR: cannot find $f"; exit 1; }; f=; set -e +# --- end runfiles.bash initialization v3 --- + +set -euo pipefail + +INTERPRETER="$(rlocation "{interpreter}")" +ENTRYPOINT="$(rlocation "{entrypoint}")" +CONFIG="$(rlocation "{config}")" +MAIN="$(rlocation "{main}")" + +runfiles_export_envvars + +exec \ + "${INTERPRETER}" \ + "${ENTRYPOINT}" \ + "${CONFIG}" \ + "${MAIN}" \ + -- \ + "$@" diff --git a/perl/private/binary_wrapper.tpl b/perl/private/binary_wrapper.tpl deleted file mode 100644 index 16e60b5..0000000 --- a/perl/private/binary_wrapper.tpl +++ /dev/null @@ -1,15 +0,0 @@ -#!/bin/sh - -if [ -n "${RUNFILES_DIR+x}" ]; then - PATH_PREFIX=$RUNFILES_DIR/{workspace_name}/ -elif [ -s `dirname $0`/../../MANIFEST ]; then - PATH_PREFIX=`cd $(dirname $0); pwd`/ -elif [ -d $0.runfiles ]; then - PATH_PREFIX=`cd $0.runfiles; pwd`/{workspace_name}/ -else - PATH_PREFIX=./ -fi - -export PERL5LIB="$PERL5LIB{PERL5LIB}" - -{env_vars} $PATH_PREFIX{interpreter} -I${PATH_PREFIX} ${PATH_PREFIX}{main} "$@" diff --git a/perl/private/entrypoint.pl b/perl/private/entrypoint.pl new file mode 100644 index 0000000..c0e8f2a --- /dev/null +++ b/perl/private/entrypoint.pl @@ -0,0 +1,90 @@ +use strict; +use warnings; +use File::Spec; +use File::Temp qw/tempdir/; +use File::Path qw/make_path/; +use File::Copy qw/copy/; +use File::Basename qw/dirname/; +use Cwd 'abs_path'; +use JSON::PP; + +# Ensure enough args +die "Usage: $0 <config.json> <main.pl> -- [args...]" unless @ARGV >= 3; + +# Extract config path and main script path +my $config_path = shift @ARGV; +my $main_path = shift @ARGV; + +# Find `--` separator +my $separator_index = 0; +$separator_index++ until $separator_index >= @ARGV || $ARGV[$separator_index] eq '--'; +die "Missing -- separator after config and main script paths" if $separator_index == @ARGV; + +# Get args after -- +my @extra_args = @ARGV[ $separator_index + 1 .. $#ARGV ]; +splice(@ARGV, $separator_index); # remove args after -- + +# Load JSON config +open my $fh, '<', $config_path or die "Can't open config file '$config_path': $!"; +my $json_text = do { local $/; <$fh> }; +close $fh; + +my $config = decode_json($json_text); +my $includes = $config->{includes} // []; + +# Create RUNFILES_DIR if not set +my $runfiles = $ENV{RUNFILES_DIR}; +unless (defined $runfiles) { + my $manifest = $ENV{RUNFILES_MANIFEST_FILE} + or die "RUNFILES_DIR is not set and RUNFILES_MANIFEST_FILE is not provided.\n"; + + # Create a temporary runfiles directory + $runfiles = tempdir(CLEANUP => 1); + if (defined $ENV{RULES_PERL_DEBUG}) { + warn "[DEBUG] RUNFILES_DIR created: $runfiles\n"; + } + $ENV{RUNFILES_DIR} = $runfiles; + + # Copy entries from manifest + open my $mfh, '<', $manifest or die "Failed to open manifest file '$manifest': $!"; + while (my $line = <$mfh>) { + chomp $line; + next if $line =~ /^\s*$/; # skip blank lines + + my ($rel_path, $real_path) = split ' ', $line, 2; + die "Invalid manifest line: $line" unless defined $real_path; + + my $dst_path = File::Spec->catfile($runfiles, $rel_path); + make_path(dirname($dst_path)); + copy($real_path, $dst_path) + or die "Failed to copy '$real_path' to '$dst_path': $!"; + } + close $mfh; +} + +# Make sure RUNFILES_DIR is absolute +unless (File::Spec->file_name_is_absolute($runfiles)) { + $runfiles = File::Spec->rel2abs($runfiles); + $ENV{RUNFILES_DIR} = $runfiles; +} + +# Build include paths relative to RUNFILES_DIR +my @include_paths = map { File::Spec->catfile($runfiles, $_) } @$includes; + +# Get current Perl interpreter +my $perl = abs_path($^X); + +# Build -I include flags +my @inc_flags = map { ('-I', $_) } @include_paths; + +# Build the full command array +my @cmd = ($perl, @inc_flags, $main_path, @extra_args); + +# Debug output if RULES_PERL_DEBUG is set +if (defined $ENV{RULES_PERL_DEBUG}) { + warn "[DEBUG] Subprocess command: @cmd\n"; +} + +# Run the command in a subprocess, exit with its code +my $exit = system(@cmd); +exit($exit >> 8); diff --git a/perl/private/perl.bzl b/perl/private/perl.bzl index 3e67b23..8a35dc0 100644 --- a/perl/private/perl.bzl +++ b/perl/private/perl.bzl @@ -1,5 +1,6 @@ """Perl rules for Bazel""" +load("@bazel_skylib//lib:paths.bzl", "paths") load(":providers.bzl", "PerlInfo") _PERL_FILE_TYPES = [".pl", ".pm", ".t", ".so", ".ix", ".al", ""] @@ -35,6 +36,19 @@ _EXECUTABLE_PERL_ATTRS = _COMMON_PERL_ATTRS | { doc = "The name of the source file that is the main entry point of the application.", allow_single_file = _PERL_FILE_TYPES, ), + "_bash_runfiles": attr.label( + cfg = "target", + allow_single_file = True, + default = Label("@bazel_tools//tools/bash/runfiles"), + ), + "_entrypoint": attr.label( + doc = "The executable entrypoint.", + allow_single_file = True, + default = Label("//perl/private:entrypoint.pl"), + ), + "_windows_constraint": attr.label( + default = Label("@platforms//os:windows"), + ), "_wrapper_template": attr.label( allow_single_file = True, default = Label("//perl/private:binary_wrapper.tpl"), @@ -82,30 +96,50 @@ def _transitive_deps(ctx, extra_files = [], extra_deps = []): files = files, ) -def _include_paths(ctx): - """Calculate the PERL5LIB paths for a perl_library rule's includes.""" +def _include_paths(ctx, includes, transitive_includes): + """Determine the include paths from a target's `includes` attribute. + + Args: + ctx (ctx): The rule's context object. + includes (list): A list of include paths. + transitive_includes (depset): Resolved includes form transitive dependencies. + + Returns: + depset: A set of the resolved include paths. + """ workspace_name = ctx.label.workspace_name - if workspace_name: - workspace_root = "../" + workspace_name - else: - workspace_root = "" - package_root = (workspace_root + "/" + ctx.label.package).strip("/") or "." - include_paths = [package_root] if "." in ctx.attr.includes else [] - include_paths.extend([package_root + "/" + include for include in ctx.attr.includes if include != "."]) - for dep in ctx.attr.deps: - include_paths.extend(dep[PerlInfo].includes) - include_paths = depset(direct = include_paths).to_list() - return include_paths + if not workspace_name: + workspace_name = ctx.workspace_name + + include_root = "{}/{}".format(workspace_name, ctx.label.package).rstrip("/") + + result = [workspace_name] + for include_str in includes: + include_str = ctx.expand_make_variables("includes", include_str, {}) + if include_str.startswith("/"): + continue + + # To prevent "escaping" out of the runfiles tree, we normalize + # the path and ensure it doesn't have up-level references. + include_path = paths.normalize("{}/{}".format(include_root, include_str)) + if include_path.startswith("../") or include_path == "..": + fail("Path '{}' references a path above the execution root".format( + include_str, + )) + result.append(include_path) + + return depset(result, transitive = [transitive_includes]) def _perl_library_implementation(ctx): transitive_sources = _transitive_deps(ctx) + transitive_includes = depset(transitive = [dep[PerlInfo].includes for dep in ctx.attr.deps]) return [ DefaultInfo( runfiles = transitive_sources.files, ), PerlInfo( transitive_perl_sources = transitive_sources.srcs, - includes = _include_paths(ctx), + includes = _include_paths(ctx, ctx.attr.includes, transitive_includes), ), ] @@ -121,68 +155,81 @@ def _get_main_from_sources(ctx): fail("Cannot infer main from multiple 'srcs'. Please specify 'main' attribute.", "main") return sources[0] -def _is_identifier(name): - # Must be non-empty. - if name == None or len(name) == 0: - return False - - # Must start with alpha or '_' - if not (name[0].isalpha() or name[0] == "_"): - return False - - # Must consist of alnum characters or '_'s. - for c in name.elems(): - if not (c.isalnum() or c == "_"): - return False - return True - def _env_vars(ctx): - environment = "" + environment = {} for name, value in ctx.attr.env.items(): - if not _is_identifier(name): - fail("%s is not a valid environment variable name." % str(name)) - value = ctx.expand_location(value, targets = ctx.attr.data) - environment += ("{key}='{value}' ").format( - key = name, - value = value.replace("'", "\\'"), - ) + environment[name] = ctx.expand_location(value, targets = ctx.attr.data) return environment +def _rlocationpath(file, workspace_name): + if file.short_path.startswith("../"): + return file.short_path[len("../"):] + + return "{}/{}".format(workspace_name, file.short_path) + def _perl_binary_implementation(ctx): toolchain = ctx.toolchains["@rules_perl//perl:toolchain_type"].perl_runtime interpreter = toolchain.interpreter - transitive_sources = _transitive_deps( - ctx, - extra_files = toolchain.runtime + [ctx.outputs.executable], - ) - main = ctx.file.main if main == None: main = _get_main_from_sources(ctx) - include_paths = [] - for dep in ctx.attr.deps: - include_paths.extend(dep[PerlInfo].includes) - perl5lib = ":" + ":".join(include_paths) if include_paths else "" + extension = "" + workspace_name = ctx.label.workspace_name + if not workspace_name: + workspace_name = ctx.workspace_name + if not workspace_name: + workspace_name = "_main" + + include_paths = depset([workspace_name], transitive = [dep[PerlInfo].includes for dep in ctx.attr.deps]) + + is_windows = ctx.target_platform_has_constraint(ctx.attr._windows_constraint[platform_common.ConstraintValueInfo]) + if is_windows: + extension = ".bat" + + output = ctx.actions.declare_file("{}{}".format(ctx.label.name, extension)) + config = ctx.actions.declare_file("{}.config.json".format(ctx.label.name)) + transitive_sources = _transitive_deps(ctx, extra_files = toolchain.runtime.to_list() + [ + ctx.file._bash_runfiles, + ctx.file._entrypoint, + output, + config, + ]) + + ctx.actions.write( + output = config, + content = json.encode_indent({ + "includes": include_paths.to_list(), + "runfiles": [ + _rlocationpath(src, ctx.workspace_name) + for src in depset(transitive = [transitive_sources.srcs, toolchain.runtime]).to_list() + ], + }), + ) ctx.actions.expand_template( template = ctx.file._wrapper_template, - output = ctx.outputs.executable, + output = output, substitutions = { - "{PERL5LIB}": perl5lib, - "{env_vars}": _env_vars(ctx), - "{interpreter}": interpreter.short_path, - "{main}": main.short_path, - "{workspace_name}": ctx.label.workspace_name or ctx.workspace_name, + "{config}": _rlocationpath(config, ctx.workspace_name), + "{entrypoint}": _rlocationpath(ctx.file._entrypoint, ctx.workspace_name), + "{interpreter}": _rlocationpath(interpreter, ctx.workspace_name), + "{main}": _rlocationpath(main, ctx.workspace_name), }, is_executable = True, ) - return DefaultInfo( - executable = ctx.outputs.executable, - runfiles = transitive_sources.files, - ) + return [ + DefaultInfo( + executable = output, + files = depset([output]), + runfiles = transitive_sources.files, + ), + RunEnvironmentInfo( + environment = _env_vars(ctx), + ), + ] def _perl_test_implementation(ctx): return _perl_binary_implementation(ctx) diff --git a/perl/private/perl_xs.bzl b/perl/private/perl_xs.bzl index ffa9c5d..12dde5f 100644 --- a/perl/private/perl_xs.bzl +++ b/perl/private/perl_xs.bzl @@ -68,7 +68,7 @@ def _perl_xs_implementation(ctx): toolchain = ctx.toolchains["@rules_perl//perl:toolchain_type"].perl_runtime xsubpp = toolchain.xsubpp - toolchain_files = depset(toolchain.runtime) + toolchain_files = toolchain.runtime gen = [] cc_infos = [] diff --git a/perl/toolchain.bzl b/perl/toolchain.bzl index 2118d36..208b59f 100644 --- a/perl/toolchain.bzl +++ b/perl/toolchain.bzl @@ -7,38 +7,54 @@ generated in perl_download in repo.bzl. PerlRuntimeInfo = provider( doc = "Information about a Perl interpreter, related commands and libraries", fields = { - "interpreter": "A label which points to the Perl interpreter", - "perlopt": "A list of strings which should be passed to the interpreter", - "runtime": "A list of labels which points to runtime libraries", - "xs_headers": "The c library support code for xs modules", - "xsubpp": "A label which points to the xsubpp command", + "interpreter": "File: A label which points to the Perl interpreter", + "perlopt": "list[str]: A list of strings which should be passed to the interpreter", + "runtime": "depset[File]: A list of labels which points to runtime libraries", + "xs_headers": "depset[File]: The c library support code for xs modules", + "xsubpp": "File: A label which points to the xsubpp command", }, ) -def _find_tool(ctx, name): - cmd = None - for f in ctx.files.runtime: - if f.path.endswith("/bin/%s" % name) or f.path.endswith("/bin/%s.exe" % name) or f.path.endswith("/bin/%s.bat" % name): - cmd = f - break - if not cmd: - fail("could not locate perl tool `%s`" % name) - - return cmd - -def _find_xs_headers(ctx): - hdrs = [ - f - for f in ctx.files.runtime - if "CORE" in f.path and f.path.endswith(".h") - ] - return depset(hdrs) +def _is_tool(src, name): + endings = ( + "/bin/%s" % name, + "/bin/%s.exe" % name, + "/bin/%s.bat" % name, + ) + if src.path.endswith(endings): + return True + + return False + +def _is_xs_header(src): + if "CORE" in src.path and src.path.endswith(".h"): + return True + + return False def _perl_toolchain_impl(ctx): # Find important files and paths. - interpreter_cmd = _find_tool(ctx, "perl") - xsubpp_cmd = _find_tool(ctx, "xsubpp") - xs_headers = _find_xs_headers(ctx) + interpreter_cmd = None + xsubpp_cmd = None + xs_headers = [] + for file in ctx.files.runtime: + if interpreter_cmd == None and _is_tool(file, "perl"): + interpreter_cmd = file + continue + + if xsubpp_cmd == None and _is_tool(file, "xsubpp"): + xsubpp_cmd = file + continue + + if _is_xs_header(file): + xs_headers.append(file) + continue + + if interpreter_cmd == None: + fail("Failed to find perl interpreter.") + + if xsubpp_cmd == None: + fail("Failed to find perl xsubpp.") interpreter_cmd_path = interpreter_cmd.path if ctx.target_platform_has_constraint(ctx.attr._windows_constraint[platform_common.ConstraintValueInfo]): @@ -50,8 +66,8 @@ def _perl_toolchain_impl(ctx): perl_runtime = PerlRuntimeInfo( interpreter = interpreter_cmd, xsubpp = xsubpp_cmd, - xs_headers = xs_headers, - runtime = ctx.files.runtime, + xs_headers = depset(xs_headers), + runtime = depset(ctx.files.runtime), perlopt = ctx.attr.perlopt, ), make_variables = platform_common.TemplateVariableInfo({ @@ -84,9 +100,10 @@ def _current_perl_toolchain_impl(ctx): toolchain.make_variables, DefaultInfo( runfiles = ctx.runfiles( - files = toolchain.perl_runtime.runtime, + [], + transitive_files = toolchain.perl_runtime.runtime, ), - files = depset(toolchain.perl_runtime.runtime), + files = toolchain.perl_runtime.runtime, ), ] diff --git a/test/perl_rule_test.bzl b/test/perl_rule_test.bzl index 6d2dc9e..49f9630 100644 --- a/test/perl_rule_test.bzl +++ b/test/perl_rule_test.bzl @@ -26,7 +26,10 @@ def _perl_library_test(package): def _perl_binary_test(package): rule_test( name = "hello_world_rule_test", - generates = ["hello_world"], + generates = select({ + "@platforms//os:windows": ["hello_world.bat"], + "//conditions:default": ["hello_world"], + }), rule = package + "/hello_world:hello_world", ) @@ -34,7 +37,10 @@ def _perl_test_test(package): """Issue rule tests for perl_test.""" rule_test( name = "fibonacci_rule_test", - generates = ["fibonacci_test"], + generates = select({ + "@platforms//os:windows": ["fibonacci_test.bat"], + "//conditions:default": ["fibonacci_test"], + }), rule = package + "/fibonacci:fibonacci_test", )