File Coverage

blib/lib/Module/DataPack.pm
Criterion Covered Total %
statement 49 65 75.3
branch 9 26 34.6
condition 1 12 8.3
subroutine 5 5 100.0
pod 1 1 100.0
total 65 109 59.6


line stmt bran cond sub pod time code
1             package Module::DataPack;
2              
3 1     1   94689 use 5.010001;
  1         13  
4 1     1   5 use strict;
  1         2  
  1         21  
5 1     1   4 use warnings;
  1         2  
  1         35  
6              
7 1     1   493 use File::Slurper qw(read_binary write_binary);
  1         14222  
  1         1199  
8              
9             require Exporter;
10             our @ISA = qw(Exporter);
11             our @EXPORT_OK = qw(datapack_modules);
12              
13             our $AUTHORITY = 'cpan:PERLANCAR'; # AUTHORITY
14             our $DATE = '2021-08-29'; # DATE
15             our $DIST = 'Module-DataPack'; # DIST
16             our $VERSION = '0.222'; # VERSION
17              
18             our %SPEC;
19              
20             my $mod_re = qr/\A[A-Za-z_][A-Za-z0-9_]*(::[A-Za-z0-9_]+)*\z/;
21             my $mod_pm_re = qr!\A[A-Za-z_][A-Za-z0-9_]*(/[A-Za-z0-9_]+)*\.pm\z!;
22              
23             $SPEC{datapack_modules} = {
24             v => 1.1,
25             summary => 'Like Module::FatPack, but uses datapacking instead of fatpack',
26             description => <<'_',
27              
28             Both this module and `Module:FatPack` generate source code that embeds modules'
29             source codes and load them on-demand via require hook. The difference is that
30             the modules' source codes are put in `__DATA__` section instead of regular Perl
31             hashes (fatpack uses `%fatpacked`). This reduces compilation overhead, although
32             this is not noticeable unless when the number of embedded modules is quite
33             large. For example, in `App::pause`, the `pause` script embeds ~320 modules with
34             a total of ~54000 lines. The overhead of fatpack code is ~49ms on my PC, while
35             with datapack the overhead is about ~10ms.
36              
37             There are two downsides of this technique. The major one is that you cannot load
38             modules during BEGIN phase (e.g. using `use`) because at that point, DATA
39             section is not yet available. You can only use run-time require()'s.
40              
41             Another downside of this technique is that you cannot use `__DATA__` section for
42             other purposes (well, actually with some care, you still can).
43              
44             _
45             args_rels => {
46             req_one => ['module_names', 'module_srcs'],
47             'dep_any&' => [
48             ],
49             },
50             args => {
51             module_names => {
52             'x.name.is_plural' => 1,
53             'x.name.singular' => 'module_name',
54             summary => 'Module names to search',
55             schema => ['array*', of=>['str*', match=>$mod_re], min_len=>1],
56             tags => ['category:input'],
57             pos => 0,
58             greedy => 1,
59             'x.schema.element_entity' => 'modulename',
60             cmdline_aliases => {m=>{}},
61             },
62             module_srcs => {
63             'x.name.is_plural' => 1,
64             'x.name.singular' => 'module_src',
65             summary => 'Module source codes (a hash, keys are module names)',
66             schema => ['hash*', {
67             each_key=>['str*', match=>$mod_re],
68             each_value=>['str*'],
69             min_len=>1,
70             }],
71             tags => ['category:input'],
72             },
73             preamble => {
74             summary => 'Perl source code to add before the datapack code',
75             schema => 'str*',
76             tags => ['category:input'],
77             },
78             postamble => {
79             summary => 'Perl source code to add after the datapack code'.
80             ' (but before the __DATA__ section)',
81             schema => 'str*',
82             tags => ['category:input'],
83             },
84              
85             output => {
86             summary => 'Output filename',
87             schema => 'str*',
88             cmdline_aliases => {o=>{}},
89             tags => ['category:output'],
90             'x.schema.entity' => 'filename',
91             },
92             overwrite => {
93             summary => 'Whether to overwrite output if previously exists',
94             'summary.alt.bool.yes' => 'Overwrite output if previously exists',
95             schema => [bool => default => 0],
96             tags => ['category:output'],
97             },
98              
99             put_hook_at_the_end => {
100             summary => 'Put the require hook at the end of @INC using "push" '.
101             'instead of at the front using "unshift"',
102             schema => ['bool*', is=>1],
103             },
104             },
105             examples => [
106             {
107             summary => 'Datapack two modules',
108             src => 'datapack-modules Text::Table::Tiny Try::Tiny',
109             src_plang => 'bash',
110             test => 0,
111             'x.doc.show_result' => 0,
112             },
113              
114             ],
115             };
116             sub datapack_modules {
117 1     1 1 2184 my %args = @_;
118              
119 1   50     9 my $put_hook_at_the_end = $args{put_hook_at_the_end} // 0;
120              
121 1         3 my %module_srcs; # key: mod_pm
122 1 50       4 if ($args{module_srcs}) {
123 0         0 for my $mod (keys %{ $args{module_srcs} }) {
  0         0  
124 0 0       0 my $mod_pm = $mod; $mod_pm =~ s!::!/!g; $mod_pm .= ".pm" unless $mod_pm =~ /\.pm\z/;
  0         0  
  0         0  
125 0         0 $module_srcs{$mod_pm} = $args{module_srcs}{$mod};
126             }
127             } else {
128 1         637 require Module::Path::More;
129 1         1189 for my $mod (@{ $args{module_names} }) {
  1         5  
130 2 50       102 my $mod_pm = $mod; $mod_pm =~ s!::!/!g; $mod_pm .= ".pm" unless $mod_pm =~ /\.pm\z/;
  2         5  
  2         10  
131 2 50       19 next if $module_srcs{$mod_pm};
132 2         11 my $path = Module::Path::More::module_path(
133             module => $mod, find_pmc=>0);
134 2 50       539 die "Can't find module '$mod_pm'" unless $path;
135 2         10 $module_srcs{$mod_pm} = read_binary($path);
136             }
137             }
138              
139 1 50       142 if ($args{stripper}) {
140 0         0 require Perl::Stripper;
141             my $stripper = Perl::Stripper->new(
142             maintain_linum => $args{stripper_maintain_linum} // 0,
143             strip_ws => $args{stripper_ws} // 1,
144             strip_comment => $args{stripper_comment} // 1,
145             strip_pod => $args{stripper_pod} // 1,
146 0   0     0 strip_log => $args{stripper_log} // 0,
      0        
      0        
      0        
      0        
147             );
148 0         0 for my $mod_pm (keys %module_srcs) {
149 0         0 $module_srcs{$mod_pm} = $stripper->strip($module_srcs{$mod_pm});
150             }
151             }
152              
153 1         3 my @res;
154              
155 1 50       3 push @res, $args{preamble} if defined $args{preamble};
156              
157             # how to line number (# line): position of __DATA__ + 1 (DSS header) + number of header lines + 1 (blank line) + $order+1 (number of ### file ### header) + lineoffset
158 1         3 push @res, <<'_';
159             # BEGIN DATAPACK CODE
160             package main::_DataPacker;
161             our $handler;
162             sub main::_DataPacker::INC { goto $handler }
163              
164             package main;
165             {
166             my $toc;
167             my $data_linepos = 1;
168             _
169 1         3 push @res, <<'_';
170             $main::_DataPacker::handler = sub {
171             my $debug = $ENV{PERL_DATAPACKER_DEBUG};
172             if ($debug) {
173             my @caller0 = caller;
174             warn "[datapacker] Hook called with arguments: (".join(",", @_).") by package $caller0[0] in file $caller0[1] line $caller0[2]\n";
175             }
176              
177             $toc ||= do {
178              
179             my $fh = \*DATA;
180              
181             my $header_line;
182             my $header_found;
183             while (1) {
184             my $header_line = <$fh>;
185             defined($header_line)
186             or die "Unexpected end of data section while reading header line";
187             chomp($header_line);
188             if ($header_line eq 'Data::Section::Seekable v1') {
189             $header_found++;
190             last;
191             }
192             }
193             die "Can't find header 'Data::Section::Seekable v1'"
194             unless $header_found;
195              
196             my %toc;
197             my $i = 0;
198             while (1) {
199             $i++;
200             my $toc_line = <$fh>;
201             defined($toc_line)
202             or die "Unexpected end of data section while reading TOC line #$i";
203             chomp($toc_line);
204             $toc_line =~ /\S/ or last;
205             $toc_line =~ /^([^,]+),(\d+),(\d+)(?:,(.*))?$/
206             or die "Invalid TOC line #$i in data section: $toc_line";
207             $toc{$1} = [$2, $3, $4];
208             }
209             my $pos = tell $fh;
210             $toc{$_}[0] += $pos for keys %toc;
211              
212              
213             # calculate the line number of data section
214             my $data_pos = tell(DATA);
215             seek DATA, 0, 0;
216             my $pos = 0;
217             while (1) {
218             my $line = ;
219             $pos += length($line);
220             $data_linepos++;
221             last if $pos >= $data_pos;
222             }
223             seek DATA, $data_pos, 0;
224              
225             \%toc;
226             };
227             if ($toc->{$_[1]}) {
228             warn "[datapacker] $_[1] FOUND in packed modules\n" if $debug;
229             seek DATA, $toc->{$_[1]}[0], 0;
230             read DATA, my($content), $toc->{$_[1]}[1];
231             my ($order, $lineoffset) = split(';', $toc->{$_[1]}[2]);
232             $content =~ s/^#//gm;
233             $content = "# line ".($data_linepos + $order+1 + $lineoffset)." \"".__FILE__."\"\n" . $content;
234             open my $fh, '<', \$content
235             or die "DataPacker error loading $_[1]: $!";
236             return $fh;
237             } else {
238             warn "[datapacker] $_[1] NOT found in packed modules\n" if $debug;
239             }
240             return;
241             }; # handler
242             _
243 1 50       3 if ($put_hook_at_the_end) {
244 0         0 push @res, <<'_';
245             push @INC, bless(sub {"dummy"}, "main::_DataPacker");
246             _
247             } else {
248 1         2 push @res, <<'_';
249             unshift @INC, bless(sub {"dummy"}, "main::_DataPacker");
250             _
251             }
252 1         2 push @res, <<'_';
253             }
254             # END DATAPACK CODE
255             _
256              
257 1 50       5 push @res, $args{postamble} if defined $args{postamble};
258              
259 1         528 require Data::Section::Seekable::Writer;
260 1         804 my $writer = Data::Section::Seekable::Writer->new;
261 1         50 my $linepos = 0;
262 1         2 my $i = -1;
263 1         7 for my $mod_pm (sort keys %module_srcs) {
264 2         4 $i++;
265             my $content = join(
266             "",
267 2         68 $module_srcs{$mod_pm},
268             );
269 2         648 $content =~ s/^/#/gm;
270 2         18 $writer->add_part($mod_pm => $content, "$i;$linepos");
271 2         227 my $lines = 0; $lines++ while $content =~ /^/gm;
  2         983  
272 2         6 $linepos += $lines;
273             }
274 1         4 push @res, "\n__DATA__\n", $writer;
275              
276 1 50       5 if ($args{output}) {
277 0         0 my $outfile = $args{output};
278 0 0       0 if (-f $outfile) {
279             return [409, "Won't overwrite existing file '$outfile'"]
280 0 0       0 unless $args{overwrite};
281             }
282 0 0       0 write_binary($outfile, join("", @res))
283             or die "Can't write to '$outfile': $!";
284 0         0 return [200, "OK, written to '$outfile'"];
285             } else {
286 1         10 return [200, "OK", join("", @res)];
287             }
288             }
289              
290             require PERLANCAR::AppUtil::PerlStripper; PERLANCAR::AppUtil::PerlStripper::_add_stripper_args_to_meta($SPEC{datapack_modules});
291              
292             1;
293             # ABSTRACT: Like Module::FatPack, but uses datapacking instead of fatpack
294              
295             __END__