├── .gitignore ├── Build.PL ├── Changes ├── dist.ini ├── lib └── CPAN │ ├── Static.pm │ └── Static │ ├── Install.pm │ └── Spec.pm ├── metamerge.yml ├── script └── cpan-static └── t ├── lib └── DistGen.pm └── simple.t /.gitignore: -------------------------------------------------------------------------------- 1 | .build 2 | *.bak 3 | *.swp 4 | *.swo 5 | *.tdy 6 | *.tar.gz 7 | CPAN-Static-* 8 | -------------------------------------------------------------------------------- /Build.PL: -------------------------------------------------------------------------------- 1 | #! perl 2 | 3 | use strict; 4 | use warnings; 5 | 6 | use ExtUtils::Helpers 'make_executable'; 7 | use File::Basename 'basename'; 8 | use File::Copy 'copy'; 9 | 10 | use lib 'lib'; 11 | use CPAN::Static::Install ':all'; 12 | 13 | my %opts = opts_from_args_list(@ARGV); 14 | if (basename($0) eq 'Build.PL') { 15 | configure(%opts, static_version => supports_static_install); 16 | copy('Build.PL', 'Build'); 17 | make_executable('Build'); 18 | } else { 19 | my $command = shift || 'build'; 20 | if ($command eq 'build') { 21 | build(%opts); 22 | } elsif ($command eq 'test') { 23 | test(%opts); 24 | } elsif ($command eq 'install') { 25 | install(%opts); 26 | } else { 27 | die "Unknown subcommand $command"; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Changes: -------------------------------------------------------------------------------- 1 | Revision history for CPAN-Static 2 | 3 | {{$NEXT}} 4 | 5 | 0.006 2024-04-27 19:04:04+02:00 Europe/Brussels 6 | - Export opts_from_args_string 7 | 8 | 0.005 2024-02-29 23:58:24+01:00 Europe/Brussels 9 | - Check for static_version in configure 10 | 11 | 0.004 2023-11-18 15:05:43+01:00 Europe/Brussels 12 | - Mark Build script as executable 13 | - Add supports_static_install 14 | - Add static_version argument 15 | 16 | 0.003 2023-11-07 18:05:51+01:00 Europe/Brussels 17 | - Regenerate configure requirements 18 | 19 | 0.002 2023-11-04 15:13:10+01:00 Europe/Brussels 20 | - Improve descriptions of specification 21 | 22 | 0.001 2023-11-04 15:07:46+01:00 Europe/Brussels 23 | - 24 | -------------------------------------------------------------------------------- /dist.ini: -------------------------------------------------------------------------------- 1 | name = CPAN-Static 2 | author = Leon Timmermans 3 | license = Perl_5 4 | copyright_holder = Leon Timmermans 5 | copyright_year = 2012 6 | 7 | [Git::GatherDir] 8 | [PruneCruft] 9 | [MetaYAML] 10 | [License] 11 | [Manifest] 12 | [Readme] 13 | 14 | [AutoPrereqs] 15 | skip = File::ShareDir 16 | skip = Foo::Bar 17 | [MetaJSON] 18 | [GitHub::Meta] 19 | [Git::NextVersion] 20 | [MetaProvides::Package] 21 | 22 | [ExecDir] 23 | dir = script 24 | [BuildSelf] 25 | [MetaMergeFile] 26 | 27 | [Test::Compile] 28 | [PodSyntaxTests] 29 | 30 | [InstallGuide] 31 | [PodWeaver] 32 | [PkgVersion] 33 | [NextRelease] 34 | 35 | [CheckChangesHasContent] 36 | [Git::Check] 37 | [RunExtraTests] 38 | [TestRelease] 39 | [ConfirmRelease] 40 | 41 | [MinimumPerl] 42 | 43 | [UploadToCPAN] 44 | 45 | [Git::Commit] 46 | [Git::Tag] 47 | [Git::Push] 48 | 49 | [OnlyCorePrereqs] 50 | :version = 0.012 51 | check_dual_life_versions = 0 52 | skip = ExtUtils::Config 53 | skip = ExtUtils::Helpers 54 | skip = ExtUtils::InstallPaths 55 | -------------------------------------------------------------------------------- /lib/CPAN/Static.pm: -------------------------------------------------------------------------------- 1 | package CPAN::Static; 2 | 3 | use strict; 4 | use warnings; 5 | 6 | 1; 7 | 8 | # ABSTRACT: Static Installation for CPAN distributions 9 | 10 | =head1 DESCRIPTION 11 | 12 | This distribution contains a L of static CPAN installation, as well as a reference L for it. 13 | -------------------------------------------------------------------------------- /lib/CPAN/Static/Install.pm: -------------------------------------------------------------------------------- 1 | package CPAN::Static::Install; 2 | 3 | use strict; 4 | use warnings; 5 | 6 | use Exporter 5.57 'import'; 7 | our @EXPORT_OK = qw/configure build test install supports_static_install opts_from_args_list opts_from_args_string/; 8 | our %EXPORT_TAGS = ( 9 | 'all' => \@EXPORT_OK, 10 | ); 11 | 12 | use CPAN::Meta; 13 | use ExtUtils::Config 0.003; 14 | use ExtUtils::Helpers 0.020 qw/make_executable split_like_shell man1_pagename man3_pagename detildefy/; 15 | use ExtUtils::Install qw/pm_to_blib/; 16 | use ExtUtils::InstallPaths 0.002; 17 | use File::Basename qw/dirname/; 18 | use File::Find (); 19 | use File::Path qw/mkpath/; 20 | use File::Spec::Functions qw/catfile catdir rel2abs abs2rel splitdir curdir/; 21 | use Getopt::Long 2.36 qw/GetOptionsFromArray/; 22 | use JSON::PP 2 qw/encode_json decode_json/; 23 | use Scalar::Util 'blessed'; 24 | 25 | sub write_file { 26 | my ($filename, $content) = @_; 27 | open my $fh, '>', $filename or die "Could not open $filename: $!\n"; 28 | print $fh $content; 29 | } 30 | sub read_file { 31 | my ($filename) = @_; 32 | open my $fh, '<', $filename or die "Could not open $filename: $!\n"; 33 | return do { local $/; <$fh> }; 34 | } 35 | 36 | my @getopt_flags = qw/install_base=s install_path=s% installdirs=s destdir=s prefix=s config=s% 37 | uninst:1 verbose:1 dry_run:1 pureperl-only:1 create_packlist=i jobs=i/; 38 | 39 | sub opts_from_args_list { 40 | my (@args) = @_; 41 | GetOptionsFromArray(\@args, \my %result, @getopt_flags); 42 | return %result; 43 | } 44 | 45 | sub opts_from_args_string { 46 | my $arg = shift; 47 | my @args = defined $arg ? split_like_shell($arg) : (); 48 | return opts_from_args_list(@args); 49 | } 50 | 51 | sub supports_static_install { 52 | my $meta = shift; 53 | if (!$meta) { 54 | return undef unless -f 'META.json'; 55 | $meta = CPAN::Meta->load_file('META.json'); 56 | } 57 | my $static_version = $meta->custom('x_static_install') || 0; 58 | return $static_version == 1 ? $static_version : undef; 59 | } 60 | 61 | sub configure { 62 | my %args = @_; 63 | die "Unsupported static install version" if defined $args{static_version} and int $args{static_version} != 1; 64 | $args{config} = $args{config}->values_set if blessed($args{config}); 65 | my $meta = CPAN::Meta->load_file('META.json'); 66 | my %env = opts_from_args_string($ENV{PERL_MB_OPT}); 67 | printf "Saving configuration for '%s' version '%s'\n", $meta->name, $meta->version; 68 | write_file('_static_build_params', encode_json([ \%env, \%args ])); 69 | $meta->save('MYMETA.json'); 70 | } 71 | 72 | sub manify { 73 | my ($input_file, $output_file, $section, $opts) = @_; 74 | return if -e $output_file && -M $input_file <= -M $output_file; 75 | my $dirname = dirname($output_file); 76 | mkpath($dirname, $opts->{verbose}) if not -d $dirname; 77 | require Pod::Man; 78 | Pod::Man->new(section => $section)->parse_from_file($input_file, $output_file); 79 | print "Manifying $output_file\n" if $opts->{verbose} && $opts->{verbose} > 0; 80 | return; 81 | } 82 | 83 | sub find { 84 | my ($pattern, $dir) = @_; 85 | my @result; 86 | File::Find::find(sub { push @result, $File::Find::name if /$pattern/ && -f }, $dir) if -d $dir; 87 | return @result; 88 | } 89 | 90 | sub contains_pod { 91 | my ($file) = @_; 92 | return unless -T $file; 93 | return read_file($file) =~ /^\=(?:head|pod|item)/m; 94 | } 95 | 96 | sub hash_merge { 97 | my ($left, @others) = @_; 98 | my %result = %{$left}; 99 | for my $right (@others) { 100 | for my $key (keys %$right) { 101 | $result{$key} = ref($right->{$key}) eq 'HASH' ? hash_merge($result{key}, $right->{key}) : $right->{$key}; 102 | } 103 | } 104 | return %result; 105 | } 106 | 107 | sub get_opts { 108 | my %extra_opts = @_; 109 | my ($env, $bargv) = @{ decode_json(read_file('_static_build_params')) }; 110 | my %options = hash_merge($env, $bargv, \%extra_opts); 111 | $_ = detildefy($_) for grep { defined } @options{qw/install_base destdir prefix/}, values %{ $options{install_path} }; 112 | $options{meta} = CPAN::Meta->load_file('MYMETA.json'); 113 | $options{config} = ExtUtils::Config->new($options{config}); 114 | $options{install_paths} = ExtUtils::InstallPaths->new(%options, dist_name => $options{meta}->name); 115 | return %options; 116 | } 117 | 118 | sub build { 119 | my %extra_opts = @_; 120 | my %opt = get_opts(%extra_opts); 121 | my %modules = map { $_ => catfile('blib', $_) } find(qr/\.pm$/, 'lib'); 122 | my %docs = map { $_ => catfile('blib', $_) } find(qr/\.pod$/, 'lib'); 123 | my %scripts = map { $_ => catfile('blib', $_) } find(qr/(?:)/, 'script'); 124 | my %sdocs = map { $_ => delete $scripts{$_} } grep { /.pod$/ } keys %scripts; 125 | my %dist_shared = map { $_ => catfile(qw/blib lib auto share dist/, $opt{meta}->name, abs2rel($_, 'share')) } find(qr/(?:)/, 'share'); 126 | my %module_shared = map { $_ => catfile(qw/blib lib auto share module/, abs2rel($_, 'module-share')) } find(qr/(?:)/, 'module-share'); 127 | pm_to_blib({ %modules, %docs, %scripts, %dist_shared, %module_shared }, catdir(qw/blib lib auto/)); 128 | make_executable($_) for values %scripts; 129 | mkpath(catdir(qw/blib arch/), $opt{verbose}); 130 | 131 | if ($opt{install_paths}->is_default_installable('bindoc')) { 132 | my $section = $opt{config}->get('man1ext'); 133 | for my $input (keys %scripts, keys %sdocs) { 134 | next unless contains_pod($input); 135 | my $output = catfile('blib', 'bindoc', man1_pagename($input)); 136 | manify($input, $output, $section, \%opt); 137 | } 138 | } 139 | if ($opt{install_paths}->is_default_installable('libdoc')) { 140 | my $section = $opt{config}->get('man3ext'); 141 | for my $input (keys %modules, keys %docs) { 142 | next unless contains_pod($input); 143 | my $output = catfile('blib', 'libdoc', man3_pagename($input)); 144 | manify($input, $output, $section, \%opt); 145 | } 146 | } 147 | } 148 | 149 | sub test { 150 | my %extra_opts = @_; 151 | my %opt = get_opts(%extra_opts); 152 | die "Must run `./Build build` first\n" if not -d 'blib'; 153 | require TAP::Harness::Env; 154 | my %test_args = ( 155 | (verbosity => $opt{verbose}) x!! exists $opt{verbose}, 156 | (jobs => $opt{jobs}) x!! exists $opt{jobs}, 157 | (color => 1) x !!-t STDOUT, 158 | lib => [ map { rel2abs(catdir(qw/blib/, $_)) } qw/arch lib/ ], 159 | ); 160 | my $tester = TAP::Harness::Env->create(\%test_args); 161 | $tester->runtests(sort +find(qr/\.t$/, 't'))->has_errors and die "Tests failed"; 162 | } 163 | 164 | sub install { 165 | my (%extra_opts) = @_; 166 | my %opt = get_opts(%extra_opts); 167 | die "Must run `./Build build` first\n" if not -d 'blib'; 168 | ExtUtils::Install::install($opt{install_paths}->install_map, @opt{qw/verbose dry_run uninst/}); 169 | } 170 | 171 | 1; 172 | 173 | # ABSTRACT: static CPAN installation reference implementation 174 | 175 | =head1 SYNOPSIS 176 | 177 | if (my $static = supports_static_install($meta)) { 178 | configure(static_version => $static); 179 | ... install dependencies ... 180 | build; 181 | test; 182 | install; 183 | } else { 184 | ... 185 | } 186 | 187 | =head1 DESCRIPTION 188 | 189 | This module provides a reference implementation of the L. 190 | 191 | =func supports_static_install($meta) 192 | 193 | This returns returns the version of the CPAN::Static spec for this dist. It returns undef if no version is declared or if the declared version is not supported. C<$meta> is a L object, if undefined it will be loaded from F. 194 | 195 | =func configure(%options) 196 | 197 | This function takes the following options, whose semantics are mostly described in detail in L. 198 | 199 | =over 4 200 | 201 | =item * static_version 202 | 203 | The version of the CPAN::Static spec to use, as returned by C. 204 | 205 | =item * destdir 206 | 207 | A string containing the destination directory 208 | 209 | =item * installdirs 210 | 211 | The type of installdirs, one of C<'site'>, C<'vendor'> or C<'core'>. 212 | 213 | =item * install_base 214 | 215 | The path to the install base. 216 | 217 | =item * install_path 218 | 219 | A hash describing the install path for different target types. 220 | 221 | =item * uninst 222 | 223 | A boolean value enabling uninstalling older versions. 224 | 225 | =item * verbose 226 | 227 | The verbosity of the actions. 228 | 229 | =item * config 230 | 231 | C<%Config> entries to be override. This should either be a hash of overrides, or an L object. 232 | 233 | =item * jobs 234 | 235 | Suggest a certain number of jobs to be run in parallel. 236 | 237 | =back 238 | 239 | =func build() 240 | 241 | This will build the dist. 242 | 243 | =func test() 244 | 245 | This will run the tests for the distribution. 246 | 247 | =func install() 248 | 249 | This will install the dist. 250 | 251 | =func opts_from_args_list 252 | 253 | This turns a list of arguments into a C<%options> hash for configure, the same way a Build.PL implementation would. It takes them as an array, e.g. C<( '--install_base', '~/foo')>. 254 | 255 | =func opts_from_args_string 256 | 257 | This turns a list of arguments into a C<%options> hash for configure, the same way a Build.PL implementation would. It takes them as an string, e.g. C<'--install_base ~/foo'>. 258 | 259 | -------------------------------------------------------------------------------- /lib/CPAN/Static/Spec.pm: -------------------------------------------------------------------------------- 1 | package CPAN::Static::Spec; 2 | 3 | use strict; 4 | use warnings; 5 | 6 | 1; 7 | 8 | # ABSTRACT: Static install specification for CPAN distributions 9 | 10 | __END__ 11 | 12 | =head1 DESCRIPTION 13 | 14 | *THIS DOCUMENT IS STILL A DRAFT* 15 | 16 | This document describes a way for CPAN clients to install 17 | distributions without having to run a F or a F 18 | 19 | =head1 PURPOSE 20 | 21 | Historically, Perl distributions have always been able to build, test 22 | and install without any help of a CPAN client. C, 23 | C, C, C. This is a powerful feature, 24 | but it is overly complicated for many modules that have no 25 | non-standard needs. 26 | 27 | =head1 CONTEXT 28 | 29 | This specification relies on a number of other specifications. This 30 | includes in particular on the L, 31 | L. The terms B, B, 32 | B and their negations have the usual IETF semantics 33 | L. 34 | 35 | =head1 AUTHOR TOOL REQUIREMENTS 36 | 37 | As static install intends to be an optimization, a valid F 38 | (per CPAN::API::BuildPL) or F B be present as a 39 | fallback. 40 | 41 | =head1 META REQUIREMENTS 42 | 43 | The author tool will add an C entry to the C. 44 | This entry will contain the version of the C that 45 | it expects the install tool to perform. The install tool C 46 | perform a static install if it doesn't support the specified version. 47 | 48 | =head1 FLOW OF EXECUTION 49 | 50 | Building a distribution has four stages. They B be performed in 51 | order, and any error in one stage B abort the entire process, 52 | unless the user explicitly asks otherwise; the CPAN client B try 53 | to fall back on dynamic install on error. Actions B be done 54 | during build-time unless noted otherwise. The order of different 55 | actions within the same phase is unspecified. Arguments that would be 56 | passed to a stage for a dynamic install B be handled by the CPAN 57 | client exactly as in CPAN::API::BuildPL. 58 | 59 | =head2 Configuration 60 | 61 | The cpan client B be able to configure a distribution. A valid 62 | F (with the C key set to C<0>) B be 63 | generated, it B be copied verbatim to from F. The same 64 | may be done for F/F. This action B be done 65 | during configure-time. 66 | 67 | 68 | =head2 Building 69 | 70 | Various actions must or may be performed during the building stage. 71 | 72 | =over 4 73 | 74 | =item * module files 75 | 76 | The cpan client B be able to build and install modules. It 77 | B look recursively in F for all F<*.pm> and F<*.pod> files 78 | and copy these to the appropriate location for C files during 79 | install. If applicable, these modules B be autosplit and their 80 | permissions B be set appropriately for that platform. 81 | 82 | =item * script files 83 | 84 | The cpan client B be able to build and install scripts. It 85 | B look non-recursively in F