From 0cdbf925402252b65e4a5e72f72f6208f4d9c290 Mon Sep 17 00:00:00 2001 From: UebelAndre Date: Wed, 23 Apr 2025 04:47:33 -0700 Subject: [PATCH 01/17] Added module extension for fetching CPAN dependencies --- .bazelci/presubmit.yml | 14 + MODULE.bazel | 10 + perl/cpan/3rdparty/BUILD.bazel | 13 + perl/cpan/3rdparty/cpanfile | 3 + perl/cpan/3rdparty/cpanfile.snapshot | 468 ++++++++++++++++++ .../cpan/3rdparty/cpanfile.snapshot.lock.json | 206 ++++++++ perl/cpan/BUILD.bazel | 0 perl/cpan/extensions.bzl | 40 ++ perl/cpan/perl_cpan_compiler.bzl | 57 +++ perl/cpan/private/BUILD.bazel | 11 + perl/cpan/private/carton.bzl | 199 ++++++++ perl/cpan/private/carton_compiler.pl | 193 ++++++++ 12 files changed, 1214 insertions(+) create mode 100644 perl/cpan/3rdparty/BUILD.bazel create mode 100644 perl/cpan/3rdparty/cpanfile create mode 100644 perl/cpan/3rdparty/cpanfile.snapshot create mode 100644 perl/cpan/3rdparty/cpanfile.snapshot.lock.json create mode 100644 perl/cpan/BUILD.bazel create mode 100644 perl/cpan/extensions.bzl create mode 100644 perl/cpan/perl_cpan_compiler.bzl create mode 100644 perl/cpan/private/BUILD.bazel create mode 100644 perl/cpan/private/carton.bzl create mode 100644 perl/cpan/private/carton_compiler.pl diff --git a/.bazelci/presubmit.yml b/.bazelci/presubmit.yml index 0d28519..f1eb0c9 100644 --- a/.bazelci/presubmit.yml +++ b/.bazelci/presubmit.yml @@ -17,6 +17,20 @@ tasks: test_targets: - "//..." + cpan_compile_ubuntu2204: + name: CPAN Compile + platform: ubuntu2204 + run_targets: + - "//perl/cpan/3rdparty:compiler" + - "//perl/cpan/3rdparty:compiler" + + cpan_compile_macos: + name: CPAN Compile + platform: macos_arm64 + run_targets: + - "//perl/cpan/3rdparty:compiler" + - "//perl/cpan/3rdparty:compiler" + e2e_ubuntu2204: platform: ubuntu2204 shell_commands: diff --git a/MODULE.bazel b/MODULE.bazel index d5ffca3..c7153f5 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -30,6 +30,16 @@ register_toolchains( "@rules_perl//perl:perl_windows_x86_64_toolchain", ) +cpan = use_extension("@rules_perl//perl/cpan:extensions.bzl", "perl_cpan") +cpan.install( + name = "rules_perl_deps", + lock = "//perl/cpan/3rdparty:cpanfile.snapshot.lock.json", +) +use_repo( + cpan, + "rules_perl_deps", +) + dev_repos = use_extension("@rules_perl//perl:extensions.bzl", "perl_dev_repositories", dev_dependency = True) use_repo( dev_repos, diff --git a/perl/cpan/3rdparty/BUILD.bazel b/perl/cpan/3rdparty/BUILD.bazel new file mode 100644 index 0000000..1e706ed --- /dev/null +++ b/perl/cpan/3rdparty/BUILD.bazel @@ -0,0 +1,13 @@ +load("//perl/cpan:perl_cpan_compiler.bzl", "perl_cpan_compiler") + +exports_files([ + "cpanfile", + "cpanfile.snapshot", +]) + +perl_cpan_compiler( + name = "compiler", + cpanfile = "cpanfile", + lockfile = "cpanfile.snapshot.lock.json", + visibility = ["//visibility:public"], +) diff --git a/perl/cpan/3rdparty/cpanfile b/perl/cpan/3rdparty/cpanfile new file mode 100644 index 0000000..612e249 --- /dev/null +++ b/perl/cpan/3rdparty/cpanfile @@ -0,0 +1,3 @@ +requires 'Carton'; +requires 'File::Slurp'; +requires 'JSON::PP'; diff --git a/perl/cpan/3rdparty/cpanfile.snapshot b/perl/cpan/3rdparty/cpanfile.snapshot new file mode 100644 index 0000000..9531349 --- /dev/null +++ b/perl/cpan/3rdparty/cpanfile.snapshot @@ -0,0 +1,468 @@ +# carton snapshot format: version 1.0 +DISTRIBUTIONS + CPAN-Common-Index-0.010 + pathname: D/DA/DAGOLDEN/CPAN-Common-Index-0.010.tar.gz + provides: + CPAN::Common::Index 0.010 + CPAN::Common::Index::LocalPackage 0.010 + CPAN::Common::Index::MetaDB 0.010 + CPAN::Common::Index::Mirror 0.010 + CPAN::Common::Index::Mux::Ordered 0.010 + requirements: + CPAN::DistnameInfo 0 + CPAN::Meta::YAML 0 + Carp 0 + Class::Tiny 0 + ExtUtils::MakeMaker 6.17 + File::Basename 0 + File::Copy 0 + File::Fetch 0 + File::Spec 0 + File::Temp 0.19 + File::stat 0 + HTTP::Tiny 0 + Module::Load 0 + Search::Dict 1.07 + Tie::Handle::SkipHeader 0 + URI 0 + parent 0 + perl 5.008001 + strict 0 + warnings 0 + CPAN-DistnameInfo-0.12 + pathname: G/GB/GBARR/CPAN-DistnameInfo-0.12.tar.gz + provides: + CPAN::DistnameInfo 0.12 + requirements: + ExtUtils::MakeMaker 0 + Test::More 0 + CPAN-Meta-Check-0.018 + pathname: L/LE/LEONT/CPAN-Meta-Check-0.018.tar.gz + provides: + CPAN::Meta::Check 0.018 + requirements: + CPAN::Meta::Prereqs 2.132830 + CPAN::Meta::Requirements 2.121 + Exporter 0 + ExtUtils::MakeMaker 0 + Module::Metadata 1.000023 + base 0 + perl 5.006 + strict 0 + warnings 0 + Capture-Tiny-0.50 + pathname: D/DA/DAGOLDEN/Capture-Tiny-0.50.tar.gz + provides: + Capture::Tiny 0.50 + requirements: + Carp 0 + Exporter 0 + ExtUtils::MakeMaker 6.17 + File::Spec 0 + File::Temp 0 + IO::Handle 0 + Scalar::Util 0 + perl 5.006 + strict 0 + warnings 0 + Carton-v1.0.35 + pathname: M/MI/MIYAGAWA/Carton-v1.0.35.tar.gz + provides: + Carton 1.000035 + Carton::Builder undef + Carton::CLI undef + Carton::CPANfile undef + Carton::Dependency undef + Carton::Dist undef + Carton::Dist::Core undef + Carton::Environment undef + Carton::Error undef + Carton::Error::CPANfileNotFound undef + Carton::Error::CommandExit undef + Carton::Error::CommandNotFound undef + Carton::Error::SnapshotNotFound undef + Carton::Error::SnapshotParseError undef + Carton::Index undef + Carton::Mirror undef + Carton::Package undef + Carton::Packer undef + Carton::Snapshot undef + Carton::Snapshot::Emitter undef + Carton::Snapshot::Parser undef + Carton::Tree undef + Carton::Util undef + requirements: + CPAN::Meta 2.120921 + CPAN::Meta::Requirements 2.121 + Class::Tiny 1.001 + ExtUtils::MakeMaker 0 + Getopt::Long 2.39 + JSON::PP 2.27300 + Menlo::CLI::Compat 1.9018 + Module::CPANfile 0.9031 + Module::CoreList 0 + Path::Tiny 0.033 + Try::Tiny 0.09 + parent 0.223 + perl 5.008005 + version 0.77 + Class-Tiny-1.008 + pathname: D/DA/DAGOLDEN/Class-Tiny-1.008.tar.gz + provides: + Class::Tiny 1.008 + Class::Tiny::Object 1.008 + requirements: + Carp 0 + ExtUtils::MakeMaker 6.17 + perl 5.006 + strict 0 + warnings 0 + ExtUtils-Config-0.010 + pathname: L/LE/LEONT/ExtUtils-Config-0.010.tar.gz + provides: + ExtUtils::Config 0.010 + ExtUtils::Config::MakeMaker 0.010 + requirements: + Data::Dumper 0 + ExtUtils::MakeMaker 0 + ExtUtils::MakeMaker::Config 0 + perl 5.006 + strict 0 + warnings 0 + ExtUtils-Helpers-0.028 + pathname: L/LE/LEONT/ExtUtils-Helpers-0.028.tar.gz + provides: + ExtUtils::Helpers 0.028 + ExtUtils::Helpers::Unix 0.028 + ExtUtils::Helpers::VMS 0.028 + ExtUtils::Helpers::Windows 0.028 + requirements: + Carp 0 + Exporter 5.57 + ExtUtils::MakeMaker 0 + File::Basename 0 + File::Copy 0 + File::Spec::Functions 0 + Text::ParseWords 3.24 + strict 0 + warnings 0 + ExtUtils-InstallPaths-0.014 + pathname: L/LE/LEONT/ExtUtils-InstallPaths-0.014.tar.gz + provides: + ExtUtils::InstallPaths 0.014 + requirements: + Carp 0 + ExtUtils::Config 0.009 + ExtUtils::MakeMaker 0 + File::Spec 0 + perl 5.008 + strict 0 + warnings 0 + ExtUtils-MakeMaker-CPANfile-0.09 + pathname: I/IS/ISHIGAKI/ExtUtils-MakeMaker-CPANfile-0.09.tar.gz + provides: + ExtUtils::MakeMaker::CPANfile 0.09 + requirements: + CPAN::Meta::Converter 2.141170 + Cwd 0 + ExtUtils::MakeMaker 6.17 + File::Path 0 + Module::CPANfile 0 + Test::More 0.88 + version 0.76 + File-Slurp-9999.32 + pathname: C/CA/CAPOEIRAB/File-Slurp-9999.32.tar.gz + provides: + File::Slurp 9999.32 + requirements: + B 0 + Carp 0 + Errno 0 + Exporter 5.57 + ExtUtils::MakeMaker 0 + Fcntl 0 + File::Basename 0 + File::Spec 3.01 + File::Temp 0 + IO::Handle 0 + POSIX 0 + strict 0 + warnings 0 + File-Which-1.27 + pathname: P/PL/PLICEASE/File-Which-1.27.tar.gz + provides: + File::Which 1.27 + requirements: + ExtUtils::MakeMaker 0 + base 0 + perl 5.006 + File-pushd-1.016 + pathname: D/DA/DAGOLDEN/File-pushd-1.016.tar.gz + provides: + File::pushd 1.016 + requirements: + Carp 0 + Cwd 0 + Exporter 0 + ExtUtils::MakeMaker 6.17 + File::Path 0 + File::Spec 0 + File::Temp 0 + overload 0 + perl 5.006 + strict 0 + warnings 0 + HTTP-Tinyish-0.19 + pathname: M/MI/MIYAGAWA/HTTP-Tinyish-0.19.tar.gz + provides: + HTTP::Tinyish 0.19 + HTTP::Tinyish::Base undef + HTTP::Tinyish::Curl undef + HTTP::Tinyish::HTTPTiny undef + HTTP::Tinyish::LWP undef + HTTP::Tinyish::Wget undef + requirements: + ExtUtils::MakeMaker 0 + File::Which 0 + HTTP::Tiny 0.055 + IPC::Run3 0 + parent 0 + perl 5.008001 + IPC-Run3-0.049 + pathname: R/RJ/RJBS/IPC-Run3-0.049.tar.gz + provides: + IPC::Run3 0.049 + requirements: + ExtUtils::MakeMaker 0 + Test::More 0.31 + Time::HiRes 0 + MIME-Base32-1.303 + pathname: R/RE/REHSACK/MIME-Base32-1.303.tar.gz + provides: + MIME::Base32 1.303 + requirements: + Exporter 0 + ExtUtils::MakeMaker 0 + perl 5.008001 + utf8 0 + Menlo-1.9019 + pathname: M/MI/MIYAGAWA/Menlo-1.9019.tar.gz + provides: + Menlo 1.9019 + Menlo::Builder::Static undef + Menlo::Dependency undef + Menlo::Index::MetaCPAN undef + Menlo::Index::MetaDB 1.9019 + Menlo::Index::Mirror undef + Menlo::Util undef + requirements: + CPAN::Common::Index 0.006 + CPAN::DistnameInfo 0 + CPAN::Meta 2.132830 + CPAN::Meta::Check 0 + CPAN::Meta::Requirements 0 + CPAN::Meta::YAML 0 + Capture::Tiny 0 + Class::Tiny 1.001 + Exporter 0 + ExtUtils::Config 0.003 + ExtUtils::Helpers 0.020 + ExtUtils::InstallPaths 0.002 + ExtUtils::MakeMaker 0 + File::Temp 0 + File::Which 0 + File::pushd 0 + Getopt::Long 2.36 + HTTP::Tiny 0.054 + HTTP::Tinyish 0.04 + JSON::PP 2 + Module::CPANfile 0 + Module::CoreList 0 + Module::Metadata 0 + Parse::CPAN::Meta 0 + Parse::PMFile 0.26 + String::ShellQuote 0 + URI 0 + Win32::ShellQuote 0 + local::lib 0 + parent 0 + perl 5.008001 + version 0 + Menlo-Legacy-1.9022 + pathname: M/MI/MIYAGAWA/Menlo-Legacy-1.9022.tar.gz + provides: + Menlo::CLI::Compat 1.9022 + Menlo::Legacy 1.9022 + requirements: + ExtUtils::MakeMaker 0 + Menlo 1.9018 + perl 5.008001 + version 0.9905 + Module-CPANfile-1.1004 + pathname: M/MI/MIYAGAWA/Module-CPANfile-1.1004.tar.gz + provides: + Module::CPANfile 1.1004 + Module::CPANfile::Environment undef + Module::CPANfile::Prereq undef + Module::CPANfile::Prereqs undef + Module::CPANfile::Requirement undef + requirements: + CPAN::Meta 2.12091 + CPAN::Meta::Prereqs 2.12091 + ExtUtils::MakeMaker 0 + parent 0 + Parse-PMFile-0.47 + pathname: I/IS/ISHIGAKI/Parse-PMFile-0.47.tar.gz + provides: + Parse::PMFile 0.47 + requirements: + Dumpvalue 0 + ExtUtils::MakeMaker 0 + ExtUtils::MakeMaker::CPANfile 0.09 + File::Spec 0 + JSON::PP 2.00 + Safe 0 + version 0.83 + Path-Tiny-0.148 + pathname: D/DA/DAGOLDEN/Path-Tiny-0.148.tar.gz + provides: + Path::Tiny 0.148 + Path::Tiny::Error 0.148 + requirements: + Carp 0 + Cwd 0 + Digest 1.03 + Digest::SHA 5.45 + Encode 0 + Exporter 5.57 + ExtUtils::MakeMaker 6.17 + Fcntl 0 + File::Compare 0 + File::Copy 0 + File::Glob 0 + File::Path 2.07 + File::Spec 0.86 + File::Temp 0.19 + File::stat 0 + constant 0 + overload 0 + perl 5.008001 + strict 0 + warnings 0 + warnings::register 0 + String-ShellQuote-1.04 + pathname: R/RO/ROSCH/String-ShellQuote-1.04.tar.gz + provides: + String::ShellQuote 1.04 + requirements: + ExtUtils::MakeMaker 0 + Tie-Handle-Offset-0.004 + pathname: D/DA/DAGOLDEN/Tie-Handle-Offset-0.004.tar.gz + provides: + Tie::Handle::Offset 0.004 + Tie::Handle::SkipHeader 0.004 + requirements: + ExtUtils::MakeMaker 6.17 + Tie::Handle 0 + perl 5.006 + strict 0 + Try-Tiny-0.32 + pathname: E/ET/ETHER/Try-Tiny-0.32.tar.gz + provides: + Try::Tiny 0.32 + requirements: + Carp 0 + Exporter 5.57 + ExtUtils::MakeMaker 0 + constant 0 + perl 5.006 + strict 0 + warnings 0 + URI-5.31 + pathname: O/OA/OALDERS/URI-5.31.tar.gz + provides: + URI 5.31 + URI::Escape 5.31 + URI::Heuristic 5.31 + URI::IRI 5.31 + URI::QueryParam 5.31 + URI::Split 5.31 + URI::URL 5.31 + URI::WithBase 5.31 + URI::data 5.31 + URI::file 5.31 + URI::file::Base 5.31 + URI::file::FAT 5.31 + URI::file::Mac 5.31 + URI::file::OS2 5.31 + URI::file::QNX 5.31 + URI::file::Unix 5.31 + URI::file::Win32 5.31 + URI::ftp 5.31 + URI::ftpes 5.31 + URI::ftps 5.31 + URI::geo 5.31 + URI::gopher 5.31 + URI::http 5.31 + URI::https 5.31 + URI::icap 5.31 + URI::icaps 5.31 + URI::irc 5.31 + URI::ircs 5.31 + URI::ldap 5.31 + URI::ldapi 5.31 + URI::ldaps 5.31 + URI::mailto 5.31 + URI::mms 5.31 + URI::news 5.31 + URI::nntp 5.31 + URI::nntps 5.31 + URI::otpauth 5.31 + URI::pop 5.31 + URI::rlogin 5.31 + URI::rsync 5.31 + URI::rtsp 5.31 + URI::rtspu 5.31 + URI::scp 5.31 + URI::sftp 5.31 + URI::sip 5.31 + URI::sips 5.31 + URI::snews 5.31 + URI::ssh 5.31 + URI::telnet 5.31 + URI::tn3270 5.31 + URI::urn 5.31 + URI::urn::isbn 5.31 + URI::urn::oid 5.31 + requirements: + Carp 0 + Cwd 0 + Data::Dumper 0 + Encode 0 + Exporter 5.57 + ExtUtils::MakeMaker 0 + MIME::Base32 0 + MIME::Base64 2 + Net::Domain 0 + Scalar::Util 0 + constant 0 + integer 0 + overload 0 + parent 0 + perl 5.008001 + strict 0 + utf8 0 + warnings 0 + Win32-ShellQuote-0.003001 + pathname: H/HA/HAARG/Win32-ShellQuote-0.003001.tar.gz + provides: + Win32::ShellQuote 0.003001 + requirements: + perl 5.006 + local-lib-2.000029 + pathname: H/HA/HAARG/local-lib-2.000029.tar.gz + provides: + lib::core::only undef + local::lib 2.000029 + requirements: + perl 5.006 diff --git a/perl/cpan/3rdparty/cpanfile.snapshot.lock.json b/perl/cpan/3rdparty/cpanfile.snapshot.lock.json new file mode 100644 index 0000000..c6576fb --- /dev/null +++ b/perl/cpan/3rdparty/cpanfile.snapshot.lock.json @@ -0,0 +1,206 @@ +{ + "CPAN-Common-Index" : { + "dependencies" : [ + "CPAN-DistnameInfo", + "Class-Tiny", + "Tie-Handle-Offset", + "URI" + ], + "sha256" : "c43ddbb22fd42b06118fe6357f53700fbd77f531ba3c427faafbf303cbf4eaf0", + "strip_prefix" : "CPAN-Common-Index-0.010", + "url" : "https://cpan.metacpan.org/authors/id/D/DA/DAGOLDEN/CPAN-Common-Index-0.010.tar.gz" + }, + "CPAN-DistnameInfo" : { + "dependencies" : [], + "sha256" : "2f24fbe9f7eeacbc269d35fc61618322fc17be499ee0cd9018f370934a9f2435", + "strip_prefix" : "CPAN-DistnameInfo-0.12", + "url" : "https://cpan.metacpan.org/authors/id/G/GB/GBARR/CPAN-DistnameInfo-0.12.tar.gz" + }, + "CPAN-Meta-Check" : { + "dependencies" : [], + "sha256" : "f619d2df5ea0fd91c8cf83eb54acccb5e43d9e6ec1a3f727b3d0ac15d0cf378a", + "strip_prefix" : "CPAN-Meta-Check-0.018", + "url" : "https://cpan.metacpan.org/authors/id/L/LE/LEONT/CPAN-Meta-Check-0.018.tar.gz" + }, + "Capture-Tiny" : { + "dependencies" : [], + "sha256" : "ca6e8d7ce7471c2be54e1009f64c367d7ee233a2894cacf52ebe6f53b04e81e5", + "strip_prefix" : "Capture-Tiny-0.50", + "url" : "https://cpan.metacpan.org/authors/id/D/DA/DAGOLDEN/Capture-Tiny-0.50.tar.gz" + }, + "Carton" : { + "dependencies" : [ + "Class-Tiny", + "Menlo-Legacy", + "Module-CPANfile", + "Path-Tiny", + "Try-Tiny" + ], + "sha256" : "9c4558ca97cd08b69fdfb52b28c3ddc2043ef52f0627b90e53d05a4087344175", + "strip_prefix" : "Carton-v1.0.35", + "url" : "https://cpan.metacpan.org/authors/id/M/MI/MIYAGAWA/Carton-v1.0.35.tar.gz" + }, + "Class-Tiny" : { + "dependencies" : [], + "sha256" : "ee058a63912fa1fcb9a72498f56ca421a2056dc7f9f4b67837446d6421815615", + "strip_prefix" : "Class-Tiny-1.008", + "url" : "https://cpan.metacpan.org/authors/id/D/DA/DAGOLDEN/Class-Tiny-1.008.tar.gz" + }, + "ExtUtils-Config" : { + "dependencies" : [], + "sha256" : "82e7e4e90cbe380e152f5de6e3e403746982d502dd30197a123652e46610c66d", + "strip_prefix" : "ExtUtils-Config-0.010", + "url" : "https://cpan.metacpan.org/authors/id/L/LE/LEONT/ExtUtils-Config-0.010.tar.gz" + }, + "ExtUtils-Helpers" : { + "dependencies" : [], + "sha256" : "c8574875cce073e7dc5345a7b06d502e52044d68894f9160203fcaab379514fe", + "strip_prefix" : "ExtUtils-Helpers-0.028", + "url" : "https://cpan.metacpan.org/authors/id/L/LE/LEONT/ExtUtils-Helpers-0.028.tar.gz" + }, + "ExtUtils-InstallPaths" : { + "dependencies" : [ + "ExtUtils-Config" + ], + "sha256" : "ae65d20cc3c7e14b3cd790915c84510f82dfb37a4c9b88aa74b2e843af417d01", + "strip_prefix" : "ExtUtils-InstallPaths-0.014", + "url" : "https://cpan.metacpan.org/authors/id/L/LE/LEONT/ExtUtils-InstallPaths-0.014.tar.gz" + }, + "ExtUtils-MakeMaker-CPANfile" : { + "dependencies" : [ + "Module-CPANfile" + ], + "sha256" : "2c077607d4b0a108569074dff76e8168659062ada3a6af78b30cca0d40f8e275", + "strip_prefix" : "ExtUtils-MakeMaker-CPANfile-0.09", + "url" : "https://cpan.metacpan.org/authors/id/I/IS/ISHIGAKI/ExtUtils-MakeMaker-CPANfile-0.09.tar.gz" + }, + "File-Slurp" : { + "dependencies" : [], + "sha256" : "4c3c21992a9d42be3a79dd74a3c83d27d38057269d65509a2f555ea0fb2bc5b0", + "strip_prefix" : "File-Slurp-9999.32", + "url" : "https://cpan.metacpan.org/authors/id/C/CA/CAPOEIRAB/File-Slurp-9999.32.tar.gz" + }, + "File-Which" : { + "dependencies" : [], + "sha256" : "3201f1a60e3f16484082e6045c896842261fc345de9fb2e620fd2a2c7af3a93a", + "strip_prefix" : "File-Which-1.27", + "url" : "https://cpan.metacpan.org/authors/id/P/PL/PLICEASE/File-Which-1.27.tar.gz" + }, + "File-pushd" : { + "dependencies" : [], + "sha256" : "d73a7f09442983b098260df3df7a832a5f660773a313ca273fa8b56665f97cdc", + "strip_prefix" : "File-pushd-1.016", + "url" : "https://cpan.metacpan.org/authors/id/D/DA/DAGOLDEN/File-pushd-1.016.tar.gz" + }, + "HTTP-Tinyish" : { + "dependencies" : [ + "File-Which", + "IPC-Run3" + ], + "sha256" : "e9ce94a9913f9275d312ded4ddb34f76baf011b6b8d6029ff2871d5bd7bae468", + "strip_prefix" : "HTTP-Tinyish-0.19", + "url" : "https://cpan.metacpan.org/authors/id/M/MI/MIYAGAWA/HTTP-Tinyish-0.19.tar.gz" + }, + "IPC-Run3" : { + "dependencies" : [], + "sha256" : "9d048ae7b9ae63871bae976ba01e081d887392d904e5d48b04e22d35ed22011a", + "strip_prefix" : "IPC-Run3-0.049", + "url" : "https://cpan.metacpan.org/authors/id/R/RJ/RJBS/IPC-Run3-0.049.tar.gz" + }, + "MIME-Base32" : { + "dependencies" : [], + "sha256" : "ab21fa99130e33a0aff6cdb596f647e5e565d207d634ba2ef06bdbef50424e99", + "strip_prefix" : "MIME-Base32-1.303", + "url" : "https://cpan.metacpan.org/authors/id/R/RE/REHSACK/MIME-Base32-1.303.tar.gz" + }, + "Menlo" : { + "dependencies" : [ + "CPAN-Common-Index", + "CPAN-DistnameInfo", + "CPAN-Meta-Check", + "Capture-Tiny", + "Class-Tiny", + "ExtUtils-Config", + "ExtUtils-Helpers", + "ExtUtils-InstallPaths", + "File-Which", + "File-pushd", + "HTTP-Tinyish", + "Module-CPANfile", + "Parse-PMFile", + "String-ShellQuote", + "URI", + "Win32-ShellQuote", + "local-lib" + ], + "sha256" : "3b573f68e7b3a36a87c860be258599330fac248b518854dfb5657ac483dca565", + "strip_prefix" : "Menlo-1.9019", + "url" : "https://cpan.metacpan.org/authors/id/M/MI/MIYAGAWA/Menlo-1.9019.tar.gz" + }, + "Menlo-Legacy" : { + "dependencies" : [ + "Menlo" + ], + "sha256" : "a6acac3fee318a804b439de54acbc7c27f0b44cfdad8551bbc9cd45986abc201", + "strip_prefix" : "Menlo-Legacy-1.9022", + "url" : "https://cpan.metacpan.org/authors/id/M/MI/MIYAGAWA/Menlo-Legacy-1.9022.tar.gz" + }, + "Module-CPANfile" : { + "dependencies" : [], + "sha256" : "88efbe2e9a642dceaa186430fedfcf999aaf0e06f6cced28a714b8e56b514921", + "strip_prefix" : "Module-CPANfile-1.1004", + "url" : "https://cpan.metacpan.org/authors/id/M/MI/MIYAGAWA/Module-CPANfile-1.1004.tar.gz" + }, + "Parse-PMFile" : { + "dependencies" : [ + "ExtUtils-MakeMaker-CPANfile" + ], + "sha256" : "26817cf3d72e245452375dcff9e923a061ee0a40bbf060d3a08ebe60a334aaae", + "strip_prefix" : "Parse-PMFile-0.47", + "url" : "https://cpan.metacpan.org/authors/id/I/IS/ISHIGAKI/Parse-PMFile-0.47.tar.gz" + }, + "Path-Tiny" : { + "dependencies" : [], + "sha256" : "818aed754b74f399e42c238bea738e20a52af89a6e3feb58bec9d0130eea4746", + "strip_prefix" : "Path-Tiny-0.148", + "url" : "https://cpan.metacpan.org/authors/id/D/DA/DAGOLDEN/Path-Tiny-0.148.tar.gz" + }, + "String-ShellQuote" : { + "dependencies" : [], + "sha256" : "e606365038ce20d646d255c805effdd32f86475f18d43ca75455b00e4d86dd35", + "strip_prefix" : "String-ShellQuote-1.04", + "url" : "https://cpan.metacpan.org/authors/id/R/RO/ROSCH/String-ShellQuote-1.04.tar.gz" + }, + "Tie-Handle-Offset" : { + "dependencies" : [], + "sha256" : "ee9f39055dc695aa244a252f56ffd37f8be07209b337ad387824721206d2a89e", + "strip_prefix" : "Tie-Handle-Offset-0.004", + "url" : "https://cpan.metacpan.org/authors/id/D/DA/DAGOLDEN/Tie-Handle-Offset-0.004.tar.gz" + }, + "Try-Tiny" : { + "dependencies" : [], + "sha256" : "ef2d6cab0bad18e3ab1c4e6125cc5f695c7e459899f512451c8fa3ef83fa7fc0", + "strip_prefix" : "Try-Tiny-0.32", + "url" : "https://cpan.metacpan.org/authors/id/E/ET/ETHER/Try-Tiny-0.32.tar.gz" + }, + "URI" : { + "dependencies" : [ + "MIME-Base32" + ], + "sha256" : "b9c4d58b2614b8611ae03a95a6d60ed996f4b311ef3cd5a937b92f1825ecc564", + "strip_prefix" : "URI-5.31", + "url" : "https://cpan.metacpan.org/authors/id/O/OA/OALDERS/URI-5.31.tar.gz" + }, + "Win32-ShellQuote" : { + "dependencies" : [], + "sha256" : "aa74b0e3dc2d41cd63f62f853e521ffd76b8d823479a2619e22edb4049b4c0dc", + "strip_prefix" : "Win32-ShellQuote-0.003001", + "url" : "https://cpan.metacpan.org/authors/id/H/HA/HAARG/Win32-ShellQuote-0.003001.tar.gz" + }, + "local-lib" : { + "dependencies" : [], + "sha256" : "8df87a10c14c8e909c5b47c5701e4b8187d519e5251e87c80709b02bb33efdd7", + "strip_prefix" : "local-lib-2.000029", + "url" : "https://cpan.metacpan.org/authors/id/H/HA/HAARG/local-lib-2.000029.tar.gz" + } +} diff --git a/perl/cpan/BUILD.bazel b/perl/cpan/BUILD.bazel new file mode 100644 index 0000000..e69de29 diff --git a/perl/cpan/extensions.bzl b/perl/cpan/extensions.bzl new file mode 100644 index 0000000..c9dfad6 --- /dev/null +++ b/perl/cpan/extensions.bzl @@ -0,0 +1,40 @@ +"""Perl CPAN Extensions""" + +load("//perl/cpan/private:carton.bzl", "install") + +_install_tag = tag_class( + attrs = { + "lock": attr.label( + doc = "The Bazel generated lockfile associated with `cpanfile.snapshot`.", + allow_files = True, + mandatory = True, + ), + "name": attr.string( + doc = "The name of the module to create", + mandatory = True, + ), + }, +) + +def _perl_cpan_impl(module_ctx): + root_module_direct_deps = [] + for mod in module_ctx.modules: + for attrs in mod.tags.install: + root_module_direct_deps.append(install( + module_ctx = module_ctx, + attrs = attrs, + )) + + return module_ctx.extension_metadata( + reproducible = True, + root_module_direct_deps = root_module_direct_deps, + root_module_direct_dev_deps = [], + ) + +perl_cpan = module_extension( + doc = "A module for defining Perl dependencies.", + implementation = _perl_cpan_impl, + tag_classes = { + "install": _install_tag, + }, +) diff --git a/perl/cpan/perl_cpan_compiler.bzl b/perl/cpan/perl_cpan_compiler.bzl new file mode 100644 index 0000000..ced767d --- /dev/null +++ b/perl/cpan/perl_cpan_compiler.bzl @@ -0,0 +1,57 @@ +"""Rules for generating CPAN lock files""" + +def _perl_cpan_compiler_impl(ctx): + cpanfile = ctx.file.cpanfile + lockfile = ctx.file.lockfile + + if not cpanfile.is_source: + fail("`cpanfile` cannot be generated. Please update it to be a source file for {}".format(ctx.label)) + if not lockfile.is_source: + fail("`lockfile` cannot be generated. Please update it to be a source file for {}".format(ctx.label)) + + compiler = ctx.executable._compiler + executable = ctx.actions.declare_file("{}.{}".format(ctx.label.name, compiler.extension).rstrip(".")) + ctx.actions.symlink( + output = executable, + target_file = compiler, + is_executable = True, + ) + + runfiles = ctx.runfiles(files = [cpanfile]).merge(ctx.attr._compiler[DefaultInfo].default_runfiles) + + return [ + DefaultInfo( + executable = executable, + runfiles = runfiles, + ), + RunEnvironmentInfo( + environment = { + "PERL_CPAN_COMPILER_CPANFILE": cpanfile.short_path, + "PERL_CPAN_COMPILER_LOCKFILE": lockfile.short_path, + }, + ), + ] + +perl_cpan_compiler = rule( + doc = "TODO", + implementation = _perl_cpan_compiler_impl, + attrs = { + "cpanfile": attr.label( + doc = "TODO", + allow_single_file = ["cpanfile"], + mandatory = True, + ), + "lockfile": attr.label( + doc = "TODO", + allow_single_file = [".json"], + mandatory = True, + ), + "_compiler": attr.label( + doc = "TODO", + executable = True, + cfg = "target", + default = Label("//perl/cpan/private:carton_compiler"), + ), + }, + executable = True, +) diff --git a/perl/cpan/private/BUILD.bazel b/perl/cpan/private/BUILD.bazel new file mode 100644 index 0000000..f339ce5 --- /dev/null +++ b/perl/cpan/private/BUILD.bazel @@ -0,0 +1,11 @@ +load("//perl:perl_binary.bzl", "perl_binary") + +perl_binary( + name = "carton_compiler", + srcs = ["carton_compiler.pl"], + visibility = ["//visibility:public"], + deps = [ + "@rules_perl_deps//:Carton", + "@rules_perl_deps//:File-Slurp", + ], +) diff --git a/perl/cpan/private/carton.bzl b/perl/cpan/private/carton.bzl new file mode 100644 index 0000000..9cd3488 --- /dev/null +++ b/perl/cpan/private/carton.bzl @@ -0,0 +1,199 @@ +"""Bazel tools for interfacing with Carton""" + +_HUB_BUILD_FILE = """\ +load("@rules_perl//perl:perl_library.bzl", "perl_library") + +package(default_visibility = ["//visibility:public"]) + +DEPENDENCIES = {dependencies} + +perl_library( + name = "{name}", + deps = [ + "@{name}__" + dep + for dep in DEPENDENCIES + ], + includes = [], +) + +[ + alias( + name = dep, + actual = "@{name}__" + dep, + ) + for dep in DEPENDENCIES +] +""" + +def _perl_cpan_hub_impl(repository_ctx): + repository_ctx.file("WORKSPACE.bazel", """workspace(name = "{}")""".format( + repository_ctx.name, + )) + + repository_ctx.file("BUILD.bazel", _HUB_BUILD_FILE.format( + name = repository_ctx.attr.hub_name, + dependencies = json.encode_indent(repository_ctx.attr.modules, indent = " " * 4), + )) + +perl_cpan_hub = repository_rule( + doc = "A hub repository that exposes all CPAN dependencies from a `cpanfile`.", + implementation = _perl_cpan_hub_impl, + attrs = { + "hub_name": attr.string( + doc = "The name of the hub.", + mandatory = True, + ), + "modules": attr.string_list( + doc = "Dependencies to add to the hub.", + mandatory = True, + ), + }, +) + +_CPAN_MODULE_BUILD_FILE = """\ +load("@rules_perl//perl:perl_library.bzl", "perl_library") + +DEPENDENCIES = {dependencies} + +perl_library( + name = "{name}", + srcs = glob( + include = ["{output}**/*"], + exclude = [ + "BUILD", + "WORKSPACE", + "*.bazel", + "{output}t/**/*", + "{output}xt/**/*", + ], + ), + includes = {includes}, + deps = [ + "@{hub_name}__" + dep + for dep in DEPENDENCIES + ], + visibility = ["//visibility:public"], +) + +alias( + name = "{repo_name}", + actual = "{name}", + visibility = ["//visibility:public"], +) +""" + +def _perl_cpan_archive_impl(repository_ctx): + results = repository_ctx.download_and_extract( + repository_ctx.attr.urls, + sha256 = repository_ctx.attr.sha256, + stripPrefix = repository_ctx.attr.strip_prefix, + ) + + output = "" + includes = [] + lib_path = repository_ctx.path("lib") + if lib_path.exists: + includes = ["lib"] + else: + # In the event a lib directory doesn't exist, the module will need to be + # extracted to a subdirectory. + repository_ctx.delete(repository_ctx.path(".")) + output = "" + split = repository_ctx.attr.distribution.split("-") + if len(split) > 1: + output = split[0] + + repository_ctx.download_and_extract( + repository_ctx.attr.urls, + sha256 = results.sha256, + stripPrefix = repository_ctx.attr.strip_prefix, + output = output, + ) + + includes = ["."] + + repository_ctx.file("WORKSPACE.bazel", """workspace(name = "{}")""".format( + repository_ctx.name, + )) + + repository_ctx.file("BUILD.bazel", _CPAN_MODULE_BUILD_FILE.format( + name = repository_ctx.attr.distribution, + hub_name = repository_ctx.attr.hub_name, + repo_name = "{}__{}".format(repository_ctx.attr.hub_name, repository_ctx.attr.distribution), + dependencies = json.encode_indent(repository_ctx.attr.dependencies, indent = " " * 4), + output = "{}/".format(output).lstrip("/"), + includes = json.encode(includes), + )) + + return { + "dependencies": repository_ctx.attr.dependencies, + "distribution": repository_ctx.attr.distribution, + "hub_name": repository_ctx.attr.hub_name, + "name": repository_ctx.name, + "sha256": results.sha256, + "strip_prefix": repository_ctx.attr.strip_prefix, + "urls": repository_ctx.attr.urls, + } + +perl_cpan_archive = repository_rule( + doc = "A repository rule for fetching Perl modules from CPAN and instantiating a target for it.", + implementation = _perl_cpan_archive_impl, + attrs = { + "dependencies": attr.string_list( + doc = "The dependencies of the current module.", + mandatory = True, + ), + "distribution": attr.string( + doc = "The distribution of the module as described in the CPAN Metadata.", + mandatory = True, + ), + "hub_name": attr.string( + doc = "The name of the bzlmod hub repository.", + mandatory = True, + ), + "sha256": attr.string( + doc = "The expected SHA-256 of the file downloaded.", + ), + "strip_prefix": attr.string( + doc = "A directory prefix to strip from the extracted files.", + ), + "urls": attr.string_list( + doc = "A list of URLs to a file that will be made available to Bazel.", + mandatory = True, + ), + }, +) + +def install(*, module_ctx, attrs): + """Instantiate the cpan module for the given `install` tag_class attributes. + + Args: + module_ctx (module_ctx): The current module context. + attrs (struct): The attributes from the `install` tag class. + + Returns: + str: The name of the hub repository for the current tag_class. + """ + lock_file = module_ctx.path(attrs.lock) + module_ctx.watch(lock_file) + lockfile = json.decode(module_ctx.read(lock_file)) + + for module in lockfile: + repo_name = "{}__{}".format(attrs.name, module) + perl_cpan_archive( + name = repo_name, + urls = [lockfile[module]["url"]], + strip_prefix = lockfile[module]["strip_prefix"], + sha256 = lockfile[module]["sha256"], + hub_name = attrs.name, + distribution = module, + dependencies = lockfile[module]["dependencies"], + ) + + perl_cpan_hub( + name = attrs.name, + hub_name = attrs.name, + modules = lockfile.keys(), + ) + + return attrs.name diff --git a/perl/cpan/private/carton_compiler.pl b/perl/cpan/private/carton_compiler.pl new file mode 100644 index 0000000..91bdf9f --- /dev/null +++ b/perl/cpan/private/carton_compiler.pl @@ -0,0 +1,193 @@ +use strict; +use warnings; +use Carton::CLI; +use Cwd 'abs_path', 'getcwd'; +use File::Slurp; +use File::Spec; +use File::Temp qw(tempdir); +use HTTP::Tiny; +use JSON::PP; +use Time::HiRes qw(sleep); + +sub parse_args { + my %opts; + my $cwd = getcwd(); + + for my $var (qw( + PERL_CPAN_COMPILER_CPANFILE + PERL_CPAN_COMPILER_LOCKFILE + )) { + die "Environment variable $var is not set\n" unless exists $ENV{$var}; + } + + for my $key (['cpanfile', 'PERL_CPAN_COMPILER_CPANFILE'], + ['lockfile', 'PERL_CPAN_COMPILER_LOCKFILE']) { + + my ($name, $env) = @$key; + my $path = $ENV{$env}; + $path = File::Spec->rel2abs($path, $cwd); + $opts{$name} = $path; + } + + $opts{incremental} = 0; + for my $arg (@ARGV) { + if ($arg eq '--incremental') { + $opts{incremental} = 1; + } + } + + return %opts; +} + +sub deserialize_cpanfile_snapshot { + my ($content) = @_; + my %results; + my $current = ""; + my $container_name = ""; + + for my $line (split /\n/, $content) { + my $text = $line; + $text =~ s/^\s+|\s+$//g; + next if !$text || $text =~ /^#/; + + if ($container_name && $line =~ /^ {6}/) { + my ($key, $value) = split / /, $text, 2; + $results{$current}{$container_name}{$key} = $value; + next; + } + + if ($line =~ /^ {4}/) { + if ($text =~ /^pathname:\s+(.*)/) { + $results{$current}{pathname} = $1; + next; + } + if ($text eq 'provides:') { + $container_name = 'provides'; + next; + } + if ($text eq 'requirements:') { + $container_name = 'requirements'; + next; + } + } + + if ($line =~ /^ {2}/) { + $current = $text; + $results{$current} = { + provides => {}, + requirements => {}, + }; + next; + } + } + + return \%results; +} + +sub sanitize_name { + my ($module) = @_; + my ($name) = $module =~ /^(.*)-[^-]+$/; + return $name // $module; +} + +sub get_release { + my ($author, $distribution) = @_; + my $url = "http://fastapi.metacpan.org/release/$author/$distribution"; + my $max_retries = 3; + my $retry_count = 0; + my $response; + + while ($retry_count < $max_retries) { + my $ua = HTTP::Tiny->new( + timeout => 10, + agent => 'BazelRulesPerlCpanCompiler/1.0' + ); + + $response = $ua->get($url); + + if ($response->{success}) { + last; + } elsif ($response->{status} =~ /^5\d{2}$/) { + warn "5xx error received, retrying...\n"; + $retry_count++; + sleep(0.05); + } else { + die "Failed to fetch $url: $response->{status} $response->{reason}"; + } + } + + die "Failed to fetch $url after $max_retries retries: $response->{status} $response->{reason}" if $retry_count == $max_retries; + + my $data = decode_json($response->{content}); + die "No release key in response" unless $data->{release}; + + return $data->{release}; +} + +sub carton_install { + my ($cpanfile) = @_; + my $carton = Carton::CLI->new; + + my $tempdir = tempdir(CLEANUP => 1); + my $abs_tempdir = abs_path($tempdir); + + my @args = ( + '--cpanfile' => $cpanfile, + '--path' => $abs_tempdir, + ); + + $carton->cmd_install(@args); +} + +sub main { + if (exists $ENV{BUILD_WORKSPACE_DIRECTORY} && -d $ENV{BUILD_WORKSPACE_DIRECTORY}) { + chdir $ENV{BUILD_WORKSPACE_DIRECTORY} + or die "Failed to chdir to $ENV{BUILD_WORKSPACE_DIRECTORY}: $!"; + } + + my %args = parse_args(); + my $snapshot_file = "$args{cpanfile}.snapshot"; + + unless ($args{incremental}) { + carton_install($args{cpanfile}); + } else { + die "Expected snapshot file to exist for incremental mode: $snapshot_file\n" + unless -f $snapshot_file; + } + + my $content = read_file($snapshot_file); + my $snapshot = deserialize_cpanfile_snapshot($content); + + my %lockfile; + + for my $module (keys %$snapshot) { + my $data = $snapshot->{$module}; + my %dependencies; + + for my $req (keys %{$data->{requirements}}) { + for my $mod (keys %$snapshot) { + if (exists $snapshot->{$mod}{provides}{$req}) { + $dependencies{sanitize_name($mod)} = 1; + last; + } + } + } + + my ($author) = $data->{pathname} =~ m|/([^/]+)/[^/]+$|; + my $release = get_release($author, $module); + my $key = sanitize_name($release->{name}); + + $lockfile{$key} = { + dependencies => [ sort keys %dependencies ], + sha256 => $release->{checksum_sha256}, + strip_prefix => $module, + url => $release->{download_url}, + }; + } + + my $json = JSON::PP->new->canonical->pretty->encode(\%lockfile); + write_file($args{lockfile}, $json); + print "Lockfile written to: $args{lockfile}" +} + +main(); From c31e20fdea3e39191e5d06db5be6bb1201b94c0c Mon Sep 17 00:00:00 2001 From: UebelAndre Date: Wed, 23 Apr 2025 10:24:39 -0700 Subject: [PATCH 02/17] Load rules_perl deps in a separate module --- MODULE.bazel | 2 +- perl/cpan/3rdparty/bootstrap.py | 114 ++++++++++++++++++++++ perl/cpan/3rdparty/lock.py | 73 ++++++++++++++ perl/cpan/extensions.bzl | 39 +------- perl/cpan/perl_cpan_compiler.bzl | 12 ++- perl/cpan/private/carton_compiler.pl | 2 +- perl/cpan/private/perl_cpan_extension.bzl | 49 ++++++++++ 7 files changed, 248 insertions(+), 43 deletions(-) create mode 100644 perl/cpan/3rdparty/bootstrap.py create mode 100644 perl/cpan/3rdparty/lock.py create mode 100644 perl/cpan/private/perl_cpan_extension.bzl diff --git a/MODULE.bazel b/MODULE.bazel index c7153f5..04e8cf9 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -30,7 +30,7 @@ register_toolchains( "@rules_perl//perl:perl_windows_x86_64_toolchain", ) -cpan = use_extension("@rules_perl//perl/cpan:extensions.bzl", "perl_cpan") +cpan = use_extension("@rules_perl//perl/cpan/private:perl_cpan_extension.bzl", "perl_cpan_internal") cpan.install( name = "rules_perl_deps", lock = "//perl/cpan/3rdparty:cpanfile.snapshot.lock.json", diff --git a/perl/cpan/3rdparty/bootstrap.py b/perl/cpan/3rdparty/bootstrap.py new file mode 100644 index 0000000..092fb86 --- /dev/null +++ b/perl/cpan/3rdparty/bootstrap.py @@ -0,0 +1,114 @@ +"""bootstrap""" + +import json +import sys +import urllib.error +import urllib.request +from pathlib import Path + + +def deserialize_cpanfile_snapshot(content): + """Deserialize the contents of a `cpanfile.snapshot` file. + + Args: + content (str): The text from a `cpanfile.snapshot` + + Returns: + dict: A mapping of the snapshot data. + """ + results = {} + + current = "" + container_name = "" + for line in content.splitlines(): + text = line.strip() + + if not text or text.startswith("#"): + continue + + if container_name and line.startswith(" "): + key, _, value = text.partition(" ") + results[current][container_name][key] = value + continue + + if line.startswith(" "): + if text.startswith("pathname:"): + _, _, pathname = text.partition(" ") + results[current]["pathname"] = pathname + continue + if text.startswith("provides:"): + container_name = "provides" + continue + + if text.startswith("requirements:"): + container_name = "requirements" + continue + + if line.startswith(" "): + current = text + results[current] = { + "provides": {}, + "requirements": {}, + } + continue + + return results + + +METACPAN_API_ENDPOINT = "https://fastapi.metacpan.org/release" + + +def _get_release(author: str, distribution: str) -> dict[str, str]: + url = f"{METACPAN_API_ENDPOINT}/{author}/{distribution}" + try: + resp = urllib.request.urlopen(url).read().decode() + except urllib.error.HTTPError as ex: + raise RuntimeError(f"Failed to fetch {url}: {ex}") from ex + try: + return json.loads(resp)["release"] + except json.JSONDecodeError as ex: + raise RuntimeError(f"Failed to parse JSON from {url}: {ex}") from ex + except KeyError as ex: + raise RuntimeError( + f"Failed to find 'release' key in JSON from {url}: {ex}. Json:\n{resp}" + ) from ex + + +def sanitize_name(module): + name, _, _ = module.rpartition("-") + return name + + +def main() -> None: + snapshot_path = Path(sys.argv[1]) + snapshot = deserialize_cpanfile_snapshot(snapshot_path.read_text()) + + lockfile = {} + for module, data in snapshot.items(): + dependencies = set() + for req in data["requirements"]: + for mod, mod_data in snapshot.items(): + if req in mod_data["provides"]: + dependencies.add(sanitize_name(mod)) + break + author = data["pathname"].split("/")[-2] + release = _get_release(author, module) + + if "Path-Tiny" in module or "String-ShellQuote" in module: + from pprint import pprint + + pprint(release) + + lockfile[sanitize_name(release["name"])] = { + "dependencies": sorted(dependencies), + "sha256": release["checksum_sha256"], + "strip_prefix": module, + "url": release["download_url"], + } + + lockfile = snapshot_path.parent / snapshot_path.name + ".lock.json" + Path(lockfile).write_text(json.dumps(lockfile, indent=2, sort_keys=True) + "\n") + + +if __name__ == "__main__": + main() diff --git a/perl/cpan/3rdparty/lock.py b/perl/cpan/3rdparty/lock.py new file mode 100644 index 0000000..496c660 --- /dev/null +++ b/perl/cpan/3rdparty/lock.py @@ -0,0 +1,73 @@ +import json +import logging +from pathlib import Path +from typing import Final +import urllib.request +import urllib.error +import sys +import os + +METACPAN_API_ENDPOINT: Final = "https://fastapi.metacpan.org/release" + +logger = logging.getLogger(__name__) + + +class LockError(RuntimeError): + """Failed to generate Lockfile.""" + + +def _get_release(author: str, distribution: str) -> dict[str, str]: + url = f"{METACPAN_API_ENDPOINT}/{author}/{distribution}" + try: + resp = urllib.request.urlopen(url).read().decode() + except urllib.error.HTTPError as ex: + raise LockError(f"Failed to fetch {url}: {ex}") from ex + try: + return json.loads(resp)["release"] + except json.JSONDecodeError as ex: + raise LockError(f"Failed to parse JSON from {url}: {ex}") from ex + except KeyError as ex: + raise LockError( + f"Failed to find 'release' key in JSON from {url}: {ex}. Json:\n{resp}" + ) from ex + + +def lock(snapshot: Path) -> dict[str, dict[str, str]]: + lines = snapshot.read_text().splitlines() + del lines[: lines.index("DISTRIBUTIONS") + 1] + lockfile = {} + distribution: str | None = None + for line in lines: + if not distribution and not line.startswith(" " * 3): + distribution = line.strip() + elif distribution and line.strip().startswith("pathname: "): + pathname = line.strip().split(": ")[1] + author = pathname.split("/")[-2] + rel = _get_release(author, distribution) + distribution = None + logger.info(f"Adding {rel['distribution']}") + lockfile[rel["distribution"]] = { + "release": rel["name"], + "url": rel["download_url"], + "sha256": rel["checksum_sha256"], + } + return lockfile + + +if __name__ == "__main__": + logging.basicConfig(level=logging.INFO) + snapshot = Path("/Users/andrebrisco/Code/rules_perl/perl/cpan/3rdparty/cpanfile.snapshot") + if not snapshot.is_absolute(): + snapshot = Path(os.getenv("BUILD_WORKING_DIRECTORY", ".")) / snapshot + lockfile = Path( + sys.argv[2] if len(sys.argv) >= 3 else "cpanfile.snapshot.lock.json" + ) + if not lockfile.is_absolute(): + lockfile = Path(os.getenv("BUILD_WORKING_DIRECTORY", ".")) / lockfile + + try: + lock_json = lock(snapshot) + except (LockError, FileNotFoundError) as ex: + print(ex, file=sys.stderr) + sys.exit(1) + print(json.dumps(lock_json, indent=2) + "\n") diff --git a/perl/cpan/extensions.bzl b/perl/cpan/extensions.bzl index c9dfad6..00c2a97 100644 --- a/perl/cpan/extensions.bzl +++ b/perl/cpan/extensions.bzl @@ -1,40 +1,5 @@ """Perl CPAN Extensions""" -load("//perl/cpan/private:carton.bzl", "install") +load("//perl/cpan/private:perl_cpan_extension.bzl", _perl_cpan = "perl_cpan") -_install_tag = tag_class( - attrs = { - "lock": attr.label( - doc = "The Bazel generated lockfile associated with `cpanfile.snapshot`.", - allow_files = True, - mandatory = True, - ), - "name": attr.string( - doc = "The name of the module to create", - mandatory = True, - ), - }, -) - -def _perl_cpan_impl(module_ctx): - root_module_direct_deps = [] - for mod in module_ctx.modules: - for attrs in mod.tags.install: - root_module_direct_deps.append(install( - module_ctx = module_ctx, - attrs = attrs, - )) - - return module_ctx.extension_metadata( - reproducible = True, - root_module_direct_deps = root_module_direct_deps, - root_module_direct_dev_deps = [], - ) - -perl_cpan = module_extension( - doc = "A module for defining Perl dependencies.", - implementation = _perl_cpan_impl, - tag_classes = { - "install": _install_tag, - }, -) +perl_cpan = _perl_cpan diff --git a/perl/cpan/perl_cpan_compiler.bzl b/perl/cpan/perl_cpan_compiler.bzl index ced767d..9fe846e 100644 --- a/perl/cpan/perl_cpan_compiler.bzl +++ b/perl/cpan/perl_cpan_compiler.bzl @@ -33,21 +33,25 @@ def _perl_cpan_compiler_impl(ctx): ] perl_cpan_compiler = rule( - doc = "TODO", + doc = """\ +A rule for compiling a Bazel-compatible lock file from [cpanfile](https://metacpan.org/dist/Module-CPANfile/view/lib/cpanfile.pod) + +Note that when setting this target up for the first time, an empty file will need to be generated at the label passed +to the `lockfile` attribute. +""", implementation = _perl_cpan_compiler_impl, attrs = { "cpanfile": attr.label( - doc = "TODO", + doc = "The `cpanfile` describing dependencies.", allow_single_file = ["cpanfile"], mandatory = True, ), "lockfile": attr.label( - doc = "TODO", + doc = "The location of the Bazel lock file.", allow_single_file = [".json"], mandatory = True, ), "_compiler": attr.label( - doc = "TODO", executable = True, cfg = "target", default = Label("//perl/cpan/private:carton_compiler"), diff --git a/perl/cpan/private/carton_compiler.pl b/perl/cpan/private/carton_compiler.pl index 91bdf9f..639ecb8 100644 --- a/perl/cpan/private/carton_compiler.pl +++ b/perl/cpan/private/carton_compiler.pl @@ -187,7 +187,7 @@ sub main { my $json = JSON::PP->new->canonical->pretty->encode(\%lockfile); write_file($args{lockfile}, $json); - print "Lockfile written to: $args{lockfile}" + print "Lockfile written to: $args{lockfile}\n" } main(); diff --git a/perl/cpan/private/perl_cpan_extension.bzl b/perl/cpan/private/perl_cpan_extension.bzl new file mode 100644 index 0000000..4efcd2f --- /dev/null +++ b/perl/cpan/private/perl_cpan_extension.bzl @@ -0,0 +1,49 @@ +"""Perl CPAN Extensions""" + +load(":carton.bzl", "install") + +_install_tag = tag_class( + attrs = { + "lock": attr.label( + doc = "The Bazel generated lockfile associated with `cpanfile.snapshot`.", + allow_files = True, + mandatory = True, + ), + "name": attr.string( + doc = "The name of the module to create", + mandatory = True, + ), + }, +) + +def _perl_cpan_impl(module_ctx): + root_module_direct_deps = [] + for mod in module_ctx.modules: + for attrs in mod.tags.install: + root_module_direct_deps.append(install( + module_ctx = module_ctx, + attrs = attrs, + )) + + return module_ctx.extension_metadata( + reproducible = True, + root_module_direct_deps = root_module_direct_deps, + root_module_direct_dev_deps = [], + ) + +def _new_perl_cpan_extension(*, doc): + return module_extension( + doc = doc, + implementation = _perl_cpan_impl, + tag_classes = { + "install": _install_tag, + }, + ) + +perl_cpan = _new_perl_cpan_extension( + doc = "A module for defining Perl dependencies.", +) + +perl_cpan_internal = _new_perl_cpan_extension( + doc = "A module for defining rules_perl internal dependencies.", +) From 5d321682e59f80bd85c1715b6bed91a656e98e51 Mon Sep 17 00:00:00 2001 From: UebelAndre Date: Wed, 23 Apr 2025 11:17:54 -0700 Subject: [PATCH 03/17] Fix module implementation --- MODULE.bazel | 2 +- perl/cpan/extensions.bzl | 41 ++++++++++++++++++- perl/cpan/private/perl_cpan_extension.bzl | 49 ----------------------- 3 files changed, 40 insertions(+), 52 deletions(-) delete mode 100644 perl/cpan/private/perl_cpan_extension.bzl diff --git a/MODULE.bazel b/MODULE.bazel index 04e8cf9..c7153f5 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -30,7 +30,7 @@ register_toolchains( "@rules_perl//perl:perl_windows_x86_64_toolchain", ) -cpan = use_extension("@rules_perl//perl/cpan/private:perl_cpan_extension.bzl", "perl_cpan_internal") +cpan = use_extension("@rules_perl//perl/cpan:extensions.bzl", "perl_cpan") cpan.install( name = "rules_perl_deps", lock = "//perl/cpan/3rdparty:cpanfile.snapshot.lock.json", diff --git a/perl/cpan/extensions.bzl b/perl/cpan/extensions.bzl index 00c2a97..51ba3be 100644 --- a/perl/cpan/extensions.bzl +++ b/perl/cpan/extensions.bzl @@ -1,5 +1,42 @@ """Perl CPAN Extensions""" -load("//perl/cpan/private:perl_cpan_extension.bzl", _perl_cpan = "perl_cpan") +load("//perl/cpan/private:carton.bzl", "install") -perl_cpan = _perl_cpan +_install_tag = tag_class( + attrs = { + "lock": attr.label( + doc = "The Bazel generated lockfile associated with `cpanfile.snapshot`.", + allow_files = True, + mandatory = True, + ), + "name": attr.string( + doc = "The name of the module to create", + mandatory = True, + ), + }, +) + +def _perl_cpan_impl(module_ctx): + root_module_direct_deps = [] + for mod in module_ctx.modules: + for attrs in mod.tags.install: + hub = install( + module_ctx = module_ctx, + attrs = attrs, + ) + if mod.is_root: + root_module_direct_deps.append(hub) + + return module_ctx.extension_metadata( + reproducible = True, + root_module_direct_deps = root_module_direct_deps, + root_module_direct_dev_deps = [], + ) + +perl_cpan = module_extension( + doc = "A module for defining Perl dependencies.", + implementation = _perl_cpan_impl, + tag_classes = { + "install": _install_tag, + }, +) diff --git a/perl/cpan/private/perl_cpan_extension.bzl b/perl/cpan/private/perl_cpan_extension.bzl deleted file mode 100644 index 4efcd2f..0000000 --- a/perl/cpan/private/perl_cpan_extension.bzl +++ /dev/null @@ -1,49 +0,0 @@ -"""Perl CPAN Extensions""" - -load(":carton.bzl", "install") - -_install_tag = tag_class( - attrs = { - "lock": attr.label( - doc = "The Bazel generated lockfile associated with `cpanfile.snapshot`.", - allow_files = True, - mandatory = True, - ), - "name": attr.string( - doc = "The name of the module to create", - mandatory = True, - ), - }, -) - -def _perl_cpan_impl(module_ctx): - root_module_direct_deps = [] - for mod in module_ctx.modules: - for attrs in mod.tags.install: - root_module_direct_deps.append(install( - module_ctx = module_ctx, - attrs = attrs, - )) - - return module_ctx.extension_metadata( - reproducible = True, - root_module_direct_deps = root_module_direct_deps, - root_module_direct_dev_deps = [], - ) - -def _new_perl_cpan_extension(*, doc): - return module_extension( - doc = doc, - implementation = _perl_cpan_impl, - tag_classes = { - "install": _install_tag, - }, - ) - -perl_cpan = _new_perl_cpan_extension( - doc = "A module for defining Perl dependencies.", -) - -perl_cpan_internal = _new_perl_cpan_extension( - doc = "A module for defining rules_perl internal dependencies.", -) From 85c5f5520da92662e71c073e164076e52b861cba Mon Sep 17 00:00:00 2001 From: UebelAndre Date: Wed, 23 Apr 2025 11:17:59 -0700 Subject: [PATCH 04/17] Delete accidental commit --- perl/cpan/3rdparty/bootstrap.py | 114 -------------------------------- perl/cpan/3rdparty/lock.py | 73 -------------------- 2 files changed, 187 deletions(-) delete mode 100644 perl/cpan/3rdparty/bootstrap.py delete mode 100644 perl/cpan/3rdparty/lock.py diff --git a/perl/cpan/3rdparty/bootstrap.py b/perl/cpan/3rdparty/bootstrap.py deleted file mode 100644 index 092fb86..0000000 --- a/perl/cpan/3rdparty/bootstrap.py +++ /dev/null @@ -1,114 +0,0 @@ -"""bootstrap""" - -import json -import sys -import urllib.error -import urllib.request -from pathlib import Path - - -def deserialize_cpanfile_snapshot(content): - """Deserialize the contents of a `cpanfile.snapshot` file. - - Args: - content (str): The text from a `cpanfile.snapshot` - - Returns: - dict: A mapping of the snapshot data. - """ - results = {} - - current = "" - container_name = "" - for line in content.splitlines(): - text = line.strip() - - if not text or text.startswith("#"): - continue - - if container_name and line.startswith(" "): - key, _, value = text.partition(" ") - results[current][container_name][key] = value - continue - - if line.startswith(" "): - if text.startswith("pathname:"): - _, _, pathname = text.partition(" ") - results[current]["pathname"] = pathname - continue - if text.startswith("provides:"): - container_name = "provides" - continue - - if text.startswith("requirements:"): - container_name = "requirements" - continue - - if line.startswith(" "): - current = text - results[current] = { - "provides": {}, - "requirements": {}, - } - continue - - return results - - -METACPAN_API_ENDPOINT = "https://fastapi.metacpan.org/release" - - -def _get_release(author: str, distribution: str) -> dict[str, str]: - url = f"{METACPAN_API_ENDPOINT}/{author}/{distribution}" - try: - resp = urllib.request.urlopen(url).read().decode() - except urllib.error.HTTPError as ex: - raise RuntimeError(f"Failed to fetch {url}: {ex}") from ex - try: - return json.loads(resp)["release"] - except json.JSONDecodeError as ex: - raise RuntimeError(f"Failed to parse JSON from {url}: {ex}") from ex - except KeyError as ex: - raise RuntimeError( - f"Failed to find 'release' key in JSON from {url}: {ex}. Json:\n{resp}" - ) from ex - - -def sanitize_name(module): - name, _, _ = module.rpartition("-") - return name - - -def main() -> None: - snapshot_path = Path(sys.argv[1]) - snapshot = deserialize_cpanfile_snapshot(snapshot_path.read_text()) - - lockfile = {} - for module, data in snapshot.items(): - dependencies = set() - for req in data["requirements"]: - for mod, mod_data in snapshot.items(): - if req in mod_data["provides"]: - dependencies.add(sanitize_name(mod)) - break - author = data["pathname"].split("/")[-2] - release = _get_release(author, module) - - if "Path-Tiny" in module or "String-ShellQuote" in module: - from pprint import pprint - - pprint(release) - - lockfile[sanitize_name(release["name"])] = { - "dependencies": sorted(dependencies), - "sha256": release["checksum_sha256"], - "strip_prefix": module, - "url": release["download_url"], - } - - lockfile = snapshot_path.parent / snapshot_path.name + ".lock.json" - Path(lockfile).write_text(json.dumps(lockfile, indent=2, sort_keys=True) + "\n") - - -if __name__ == "__main__": - main() diff --git a/perl/cpan/3rdparty/lock.py b/perl/cpan/3rdparty/lock.py deleted file mode 100644 index 496c660..0000000 --- a/perl/cpan/3rdparty/lock.py +++ /dev/null @@ -1,73 +0,0 @@ -import json -import logging -from pathlib import Path -from typing import Final -import urllib.request -import urllib.error -import sys -import os - -METACPAN_API_ENDPOINT: Final = "https://fastapi.metacpan.org/release" - -logger = logging.getLogger(__name__) - - -class LockError(RuntimeError): - """Failed to generate Lockfile.""" - - -def _get_release(author: str, distribution: str) -> dict[str, str]: - url = f"{METACPAN_API_ENDPOINT}/{author}/{distribution}" - try: - resp = urllib.request.urlopen(url).read().decode() - except urllib.error.HTTPError as ex: - raise LockError(f"Failed to fetch {url}: {ex}") from ex - try: - return json.loads(resp)["release"] - except json.JSONDecodeError as ex: - raise LockError(f"Failed to parse JSON from {url}: {ex}") from ex - except KeyError as ex: - raise LockError( - f"Failed to find 'release' key in JSON from {url}: {ex}. Json:\n{resp}" - ) from ex - - -def lock(snapshot: Path) -> dict[str, dict[str, str]]: - lines = snapshot.read_text().splitlines() - del lines[: lines.index("DISTRIBUTIONS") + 1] - lockfile = {} - distribution: str | None = None - for line in lines: - if not distribution and not line.startswith(" " * 3): - distribution = line.strip() - elif distribution and line.strip().startswith("pathname: "): - pathname = line.strip().split(": ")[1] - author = pathname.split("/")[-2] - rel = _get_release(author, distribution) - distribution = None - logger.info(f"Adding {rel['distribution']}") - lockfile[rel["distribution"]] = { - "release": rel["name"], - "url": rel["download_url"], - "sha256": rel["checksum_sha256"], - } - return lockfile - - -if __name__ == "__main__": - logging.basicConfig(level=logging.INFO) - snapshot = Path("/Users/andrebrisco/Code/rules_perl/perl/cpan/3rdparty/cpanfile.snapshot") - if not snapshot.is_absolute(): - snapshot = Path(os.getenv("BUILD_WORKING_DIRECTORY", ".")) / snapshot - lockfile = Path( - sys.argv[2] if len(sys.argv) >= 3 else "cpanfile.snapshot.lock.json" - ) - if not lockfile.is_absolute(): - lockfile = Path(os.getenv("BUILD_WORKING_DIRECTORY", ".")) / lockfile - - try: - lock_json = lock(snapshot) - except (LockError, FileNotFoundError) as ex: - print(ex, file=sys.stderr) - sys.exit(1) - print(json.dumps(lock_json, indent=2) + "\n") From 8edd18ea6d87acb370a0c342f0210d0faaf8d125 Mon Sep 17 00:00:00 2001 From: UebelAndre Date: Mon, 28 Apr 2025 07:01:39 -0700 Subject: [PATCH 05/17] expose lock json --- perl/cpan/3rdparty/BUILD.bazel | 1 + 1 file changed, 1 insertion(+) diff --git a/perl/cpan/3rdparty/BUILD.bazel b/perl/cpan/3rdparty/BUILD.bazel index 1e706ed..22e01fd 100644 --- a/perl/cpan/3rdparty/BUILD.bazel +++ b/perl/cpan/3rdparty/BUILD.bazel @@ -3,6 +3,7 @@ load("//perl/cpan:perl_cpan_compiler.bzl", "perl_cpan_compiler") exports_files([ "cpanfile", "cpanfile.snapshot", + "cpanfile.snapshot.lock.json", ]) perl_cpan_compiler( From eb618a615eef6aaa49aa111d2f7f6d505cd7c971 Mon Sep 17 00:00:00 2001 From: UebelAndre Date: Mon, 28 Apr 2025 07:02:47 -0700 Subject: [PATCH 06/17] update name --- MODULE.bazel | 4 ++-- perl/cpan/private/BUILD.bazel | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/MODULE.bazel b/MODULE.bazel index c7153f5..d4e9078 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -32,12 +32,12 @@ register_toolchains( cpan = use_extension("@rules_perl//perl/cpan:extensions.bzl", "perl_cpan") cpan.install( - name = "rules_perl_deps", + name = "cpan_compiler_deps", lock = "//perl/cpan/3rdparty:cpanfile.snapshot.lock.json", ) use_repo( cpan, - "rules_perl_deps", + "cpan_compiler_deps", ) dev_repos = use_extension("@rules_perl//perl:extensions.bzl", "perl_dev_repositories", dev_dependency = True) diff --git a/perl/cpan/private/BUILD.bazel b/perl/cpan/private/BUILD.bazel index f339ce5..481b553 100644 --- a/perl/cpan/private/BUILD.bazel +++ b/perl/cpan/private/BUILD.bazel @@ -5,7 +5,7 @@ perl_binary( srcs = ["carton_compiler.pl"], visibility = ["//visibility:public"], deps = [ - "@rules_perl_deps//:Carton", - "@rules_perl_deps//:File-Slurp", + "@cpan_compiler_deps//:Carton", + "@cpan_compiler_deps//:File-Slurp", ], ) From 779a61a162fae646778cbd2a0a87647d669464d1 Mon Sep 17 00:00:00 2001 From: UebelAndre Date: Mon, 28 Apr 2025 07:05:21 -0700 Subject: [PATCH 07/17] Updated extension name --- MODULE.bazel | 2 +- perl/cpan/extensions.bzl | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/MODULE.bazel b/MODULE.bazel index d4e9078..d5f0bfe 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -30,7 +30,7 @@ register_toolchains( "@rules_perl//perl:perl_windows_x86_64_toolchain", ) -cpan = use_extension("@rules_perl//perl/cpan:extensions.bzl", "perl_cpan") +cpan = use_extension("@rules_perl//perl/cpan:extensions.bzl", "cpan") cpan.install( name = "cpan_compiler_deps", lock = "//perl/cpan/3rdparty:cpanfile.snapshot.lock.json", diff --git a/perl/cpan/extensions.bzl b/perl/cpan/extensions.bzl index 51ba3be..bb4eaab 100644 --- a/perl/cpan/extensions.bzl +++ b/perl/cpan/extensions.bzl @@ -16,7 +16,7 @@ _install_tag = tag_class( }, ) -def _perl_cpan_impl(module_ctx): +def _cpan_impl(module_ctx): root_module_direct_deps = [] for mod in module_ctx.modules: for attrs in mod.tags.install: @@ -33,9 +33,9 @@ def _perl_cpan_impl(module_ctx): root_module_direct_dev_deps = [], ) -perl_cpan = module_extension( - doc = "A module for defining Perl dependencies.", - implementation = _perl_cpan_impl, +cpan = module_extension( + doc = "A module for defining Perl dependencies from [CPAN](https://www.cpan.org/).", + implementation = _cpan_impl, tag_classes = { "install": _install_tag, }, From 4bbfb0277d4bb15b95721945e5ac7fe4069d08e5 Mon Sep 17 00:00:00 2001 From: UebelAndre Date: Mon, 28 Apr 2025 07:16:37 -0700 Subject: [PATCH 08/17] Updated README --- README.md | 52 ++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 36 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 13a3871..be9bb0c 100644 --- a/README.md +++ b/README.md @@ -49,25 +49,45 @@ This repository provides a hermetic [Strawberry Perl](https://strawberryperl.com ## Using Perl Modules -This is the first stab at getting a more mature set of Perl rules for Bazel. Currently, it is a manual process and, hopefully, it will be a map for automation later on. +Perl modules from [CPAN](https://www.cpan.org/) can be generated using the `perl_cpan_compiler` rule in +conjunction with the `cpan` module extension. ### Current Steps -* Manually download the module that you want to use. -* Add the actual files that you need to your repository. - * Highly recommended that you place the files in the directory structure that each Perl file is unpacked into (you may need to run `perl Makefile.PL; make` to see the final paths) - * Recommended to create a 'cpan' directory and place the files (in their required path) there. - * Test::Mock::Simple does **NOT** follow this pattern as it is being used as a practical example - please see 'Simple Pure Perl Example' section. -* Add the new module's information to the BUILD file in the root directory of all your modules. - * the target in the `deps` attribute - * At this time compiled files (result of XS) will be put in the `srcs` attribute - * the directory where the module lives in the `env` attribute for the `PERL5LIB` variable - -#### Dependencies - -The process needs to be repeated for any dependencies that the module needs. - -Eventually, this should be an automated process. +1. Create a `cpanfile` per the [Carton](https://metacpan.org/pod/Carton) documentation. +2. Create an empty `*.json` will need to be created for Bazel to use a lockfile (e.g. `cpanfile.snapshot.lock.json`) +3. Define a `perl_cpan_compiler` target: + + ```python + load("//perl/cpan:perl_cpan_compiler.bzl", "perl_cpan_compiler") + + perl_cpan_compiler( + name = "compiler", + cpanfile = "cpanfile", + lockfile = "cpanfile.snapshot.lock.json", + visibility = ["//visibility:public"], + ) + ``` + +4. `bazel run` the new target. +5. Define a new module in `MODULE.bazel` pointing to the Bazel `*.json` lock file: + + ```python + cpan = use_extension("@rules_perl//perl/cpan:extensions.bzl", "cpan") + cpan.install( + name = "cpan", + lock = "//perl/cpan/3rdparty:cpanfile.snapshot.lock.json", + ) + use_repo( + cpan, + "cpan", + ) + ``` + +### Dependencies + +Once the `cpan` module extension is defined, dependencies will be available through the name given to the module. +Using the example in the steps above, dependencies can be accessed through `@cpan//...`. (e.g. `@cpan//:DateTime`). ### Simple Pure Perl Example From 4a8faf397337a9e4ee0a3240e7c413b8fa1a2f08 Mon Sep 17 00:00:00 2001 From: UebelAndre Date: Mon, 28 Apr 2025 07:17:44 -0700 Subject: [PATCH 09/17] Added autogen header --- perl/cpan/private/carton.bzl | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/perl/cpan/private/carton.bzl b/perl/cpan/private/carton.bzl index 9cd3488..7302aa3 100644 --- a/perl/cpan/private/carton.bzl +++ b/perl/cpan/private/carton.bzl @@ -1,6 +1,8 @@ """Bazel tools for interfacing with Carton""" _HUB_BUILD_FILE = """\ +\"\"\"Autogenerated by rules_perl.\"\"\" + load("@rules_perl//perl:perl_library.bzl", "perl_library") package(default_visibility = ["//visibility:public"]) @@ -51,6 +53,8 @@ perl_cpan_hub = repository_rule( ) _CPAN_MODULE_BUILD_FILE = """\ +\"\"\"Autogenerated by rules_perl.\"\"\" + load("@rules_perl//perl:perl_library.bzl", "perl_library") DEPENDENCIES = {dependencies} From 8f16a4916e6ca71c5b241b2bc4b9599387764bd5 Mon Sep 17 00:00:00 2001 From: UebelAndre Date: Mon, 28 Apr 2025 07:21:56 -0700 Subject: [PATCH 10/17] common visibility --- perl/cpan/private/carton.bzl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/perl/cpan/private/carton.bzl b/perl/cpan/private/carton.bzl index 7302aa3..a91dff7 100644 --- a/perl/cpan/private/carton.bzl +++ b/perl/cpan/private/carton.bzl @@ -5,8 +5,6 @@ _HUB_BUILD_FILE = """\ load("@rules_perl//perl:perl_library.bzl", "perl_library") -package(default_visibility = ["//visibility:public"]) - DEPENDENCIES = {dependencies} perl_library( @@ -16,12 +14,14 @@ perl_library( for dep in DEPENDENCIES ], includes = [], + visibility = ["//visibility:public"], ) [ alias( name = dep, actual = "@{name}__" + dep, + visibility = ["//visibility:public"], ) for dep in DEPENDENCIES ] From 52fce83c81999bccf1054cccabfd36e0dc74ddae Mon Sep 17 00:00:00 2001 From: UebelAndre Date: Mon, 28 Apr 2025 07:26:51 -0700 Subject: [PATCH 11/17] Update perl/cpan/private/carton.bzl Co-authored-by: Laurenz --- perl/cpan/private/carton.bzl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/perl/cpan/private/carton.bzl b/perl/cpan/private/carton.bzl index a91dff7..6cdf0ea 100644 --- a/perl/cpan/private/carton.bzl +++ b/perl/cpan/private/carton.bzl @@ -148,7 +148,7 @@ perl_cpan_archive = repository_rule( mandatory = True, ), "distribution": attr.string( - doc = "The distribution of the module as described in the CPAN Metadata.", + doc = "The distribution of the module as described in the CPAN Metadata, e.g. `Capture-Tiny-0.48`", mandatory = True, ), "hub_name": attr.string( From 664ed1958962aaab82d744e3f30366128b8526fa Mon Sep 17 00:00:00 2001 From: UebelAndre Date: Mon, 28 Apr 2025 07:29:34 -0700 Subject: [PATCH 12/17] update names --- perl/cpan/private/carton.bzl | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/perl/cpan/private/carton.bzl b/perl/cpan/private/carton.bzl index 6cdf0ea..368603c 100644 --- a/perl/cpan/private/carton.bzl +++ b/perl/cpan/private/carton.bzl @@ -27,7 +27,7 @@ perl_library( ] """ -def _perl_cpan_hub_impl(repository_ctx): +def _cpan_hub_impl(repository_ctx): repository_ctx.file("WORKSPACE.bazel", """workspace(name = "{}")""".format( repository_ctx.name, )) @@ -37,9 +37,9 @@ def _perl_cpan_hub_impl(repository_ctx): dependencies = json.encode_indent(repository_ctx.attr.modules, indent = " " * 4), )) -perl_cpan_hub = repository_rule( +cpan_hub = repository_rule( doc = "A hub repository that exposes all CPAN dependencies from a `cpanfile`.", - implementation = _perl_cpan_hub_impl, + implementation = _cpan_hub_impl, attrs = { "hub_name": attr.string( doc = "The name of the hub.", @@ -86,7 +86,7 @@ alias( ) """ -def _perl_cpan_archive_impl(repository_ctx): +def _cpan_distribution_archive_impl(repository_ctx): results = repository_ctx.download_and_extract( repository_ctx.attr.urls, sha256 = repository_ctx.attr.sha256, @@ -139,9 +139,9 @@ def _perl_cpan_archive_impl(repository_ctx): "urls": repository_ctx.attr.urls, } -perl_cpan_archive = repository_rule( +cpan_distribution_archive = repository_rule( doc = "A repository rule for fetching Perl modules from CPAN and instantiating a target for it.", - implementation = _perl_cpan_archive_impl, + implementation = _cpan_distribution_archive_impl, attrs = { "dependencies": attr.string_list( doc = "The dependencies of the current module.", @@ -184,7 +184,7 @@ def install(*, module_ctx, attrs): for module in lockfile: repo_name = "{}__{}".format(attrs.name, module) - perl_cpan_archive( + cpan_distribution_archive( name = repo_name, urls = [lockfile[module]["url"]], strip_prefix = lockfile[module]["strip_prefix"], @@ -194,7 +194,7 @@ def install(*, module_ctx, attrs): dependencies = lockfile[module]["dependencies"], ) - perl_cpan_hub( + cpan_hub( name = attrs.name, hub_name = attrs.name, modules = lockfile.keys(), From 729ae1b3afcf98b348b8219ddbcd36f3ac228e50 Mon Sep 17 00:00:00 2001 From: UebelAndre Date: Mon, 28 Apr 2025 07:30:28 -0700 Subject: [PATCH 13/17] rename --- README.md | 8 ++++---- perl/cpan/3rdparty/BUILD.bazel | 4 ++-- perl/cpan/{perl_cpan_compiler.bzl => cpan_compiler.bzl} | 6 +++--- 3 files changed, 9 insertions(+), 9 deletions(-) rename perl/cpan/{perl_cpan_compiler.bzl => cpan_compiler.bzl} (94%) diff --git a/README.md b/README.md index be9bb0c..831c068 100644 --- a/README.md +++ b/README.md @@ -49,19 +49,19 @@ This repository provides a hermetic [Strawberry Perl](https://strawberryperl.com ## Using Perl Modules -Perl modules from [CPAN](https://www.cpan.org/) can be generated using the `perl_cpan_compiler` rule in +Perl modules from [CPAN](https://www.cpan.org/) can be generated using the `cpan_compiler` rule in conjunction with the `cpan` module extension. ### Current Steps 1. Create a `cpanfile` per the [Carton](https://metacpan.org/pod/Carton) documentation. 2. Create an empty `*.json` will need to be created for Bazel to use a lockfile (e.g. `cpanfile.snapshot.lock.json`) -3. Define a `perl_cpan_compiler` target: +3. Define a `cpan_compiler` target: ```python - load("//perl/cpan:perl_cpan_compiler.bzl", "perl_cpan_compiler") + load("//perl/cpan:cpan_compiler.bzl", "cpan_compiler") - perl_cpan_compiler( + cpan_compiler( name = "compiler", cpanfile = "cpanfile", lockfile = "cpanfile.snapshot.lock.json", diff --git a/perl/cpan/3rdparty/BUILD.bazel b/perl/cpan/3rdparty/BUILD.bazel index 22e01fd..f184c46 100644 --- a/perl/cpan/3rdparty/BUILD.bazel +++ b/perl/cpan/3rdparty/BUILD.bazel @@ -1,4 +1,4 @@ -load("//perl/cpan:perl_cpan_compiler.bzl", "perl_cpan_compiler") +load("//perl/cpan:cpan_compiler.bzl", "cpan_compiler") exports_files([ "cpanfile", @@ -6,7 +6,7 @@ exports_files([ "cpanfile.snapshot.lock.json", ]) -perl_cpan_compiler( +cpan_compiler( name = "compiler", cpanfile = "cpanfile", lockfile = "cpanfile.snapshot.lock.json", diff --git a/perl/cpan/perl_cpan_compiler.bzl b/perl/cpan/cpan_compiler.bzl similarity index 94% rename from perl/cpan/perl_cpan_compiler.bzl rename to perl/cpan/cpan_compiler.bzl index 9fe846e..e656649 100644 --- a/perl/cpan/perl_cpan_compiler.bzl +++ b/perl/cpan/cpan_compiler.bzl @@ -1,6 +1,6 @@ """Rules for generating CPAN lock files""" -def _perl_cpan_compiler_impl(ctx): +def _cpan_compiler_impl(ctx): cpanfile = ctx.file.cpanfile lockfile = ctx.file.lockfile @@ -32,14 +32,14 @@ def _perl_cpan_compiler_impl(ctx): ), ] -perl_cpan_compiler = rule( +cpan_compiler = rule( doc = """\ A rule for compiling a Bazel-compatible lock file from [cpanfile](https://metacpan.org/dist/Module-CPANfile/view/lib/cpanfile.pod) Note that when setting this target up for the first time, an empty file will need to be generated at the label passed to the `lockfile` attribute. """, - implementation = _perl_cpan_compiler_impl, + implementation = _cpan_compiler_impl, attrs = { "cpanfile": attr.label( doc = "The `cpanfile` describing dependencies.", From 0b28d2f968aa0d734ed154e814276fb86b95af14 Mon Sep 17 00:00:00 2001 From: UebelAndre Date: Mon, 28 Apr 2025 07:31:51 -0700 Subject: [PATCH 14/17] Update perl/cpan/private/carton.bzl Co-authored-by: Laurenz --- perl/cpan/private/carton.bzl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/perl/cpan/private/carton.bzl b/perl/cpan/private/carton.bzl index 368603c..abcfcc4 100644 --- a/perl/cpan/private/carton.bzl +++ b/perl/cpan/private/carton.bzl @@ -162,7 +162,7 @@ cpan_distribution_archive = repository_rule( doc = "A directory prefix to strip from the extracted files.", ), "urls": attr.string_list( - doc = "A list of URLs to a file that will be made available to Bazel.", + doc = "List of mirror URLs referencing the same file to download.", mandatory = True, ), }, From bdc56ab533e4f531f37f53f5399516dc8bd1fe77 Mon Sep 17 00:00:00 2001 From: UebelAndre Date: Mon, 28 Apr 2025 12:10:47 -0700 Subject: [PATCH 15/17] comments --- .bazelci/presubmit.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.bazelci/presubmit.yml b/.bazelci/presubmit.yml index f1eb0c9..4f6fc79 100644 --- a/.bazelci/presubmit.yml +++ b/.bazelci/presubmit.yml @@ -21,6 +21,8 @@ tasks: name: CPAN Compile platform: ubuntu2204 run_targets: + # Two compilations are performed to ensure the + # results of the first are usable by the second. - "//perl/cpan/3rdparty:compiler" - "//perl/cpan/3rdparty:compiler" @@ -28,6 +30,8 @@ tasks: name: CPAN Compile platform: macos_arm64 run_targets: + # Two compilations are performed to ensure the + # results of the first are usable by the second. - "//perl/cpan/3rdparty:compiler" - "//perl/cpan/3rdparty:compiler" From 2e510f298fde7f46b0f2076c2002b81ffdf4c6a5 Mon Sep 17 00:00:00 2001 From: UebelAndre Date: Mon, 28 Apr 2025 12:13:14 -0700 Subject: [PATCH 16/17] xs note --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 831c068..a212406 100644 --- a/README.md +++ b/README.md @@ -89,6 +89,8 @@ conjunction with the `cpan` module extension. Once the `cpan` module extension is defined, dependencies will be available through the name given to the module. Using the example in the steps above, dependencies can be accessed through `@cpan//...`. (e.g. `@cpan//:DateTime`). +Note that `xs` dependencies are currently not supported by the `cpan` extension module. + ### Simple Pure Perl Example Downloaded and unpacked: [Test::Mock::Simple](https://metacpan.org/pod/Test::Mock::Simple) From c3472cd0b47959c70df1c6616d6ba765806a7bac Mon Sep 17 00:00:00 2001 From: UebelAndre Date: Mon, 28 Apr 2025 12:16:10 -0700 Subject: [PATCH 17/17] xs docs --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a212406..ffcbd45 100644 --- a/README.md +++ b/README.md @@ -89,7 +89,7 @@ conjunction with the `cpan` module extension. Once the `cpan` module extension is defined, dependencies will be available through the name given to the module. Using the example in the steps above, dependencies can be accessed through `@cpan//...`. (e.g. `@cpan//:DateTime`). -Note that `xs` dependencies are currently not supported by the `cpan` extension module. +Note that [`xs`](https://perldoc.perl.org/perlxs) dependencies are currently not supported by the `cpan` extension module. ### Simple Pure Perl Example