1. class CompUnit::Repository::Installation does CompUnit::Repository::Locally does CompUnit::Repository::Installable {
  2. has $!cver = nqp::hllize(nqp::atkey(nqp::gethllsym('perl6', '$COMPILER_CONFIG'), 'version'));
  3. has %!loaded;
  4. has $!precomp;
  5. has $!id;
  6. has Int $!version;
  7. has %!dist-metas;
  8. has $!precomp-stores;
  9. has $!precomp-store;
  10. my $verbose := nqp::getenvhash<RAKUDO_LOG_PRECOMP>;
  11. submethod BUILD(:$!prefix, :$!lock, :$!WHICH, :$!next-repo --> Nil) { }
  12. my class InstalledDistribution is Distribution::Hash {
  13. method content($address) {
  14. my $entry = $.meta<provides>.values.first: { $_{$address}:exists };
  15. my $file = $entry
  16. ?? $.prefix.add('sources').add($entry{$address}<file>)
  17. !! $.prefix.add('resources').add($.meta<files>{$address});
  18. $file.open(:r)
  19. }
  20. }
  21. method writeable-path {
  22. $.prefix.w ?? $.prefix !! IO::Path;
  23. }
  24. method !writeable-path {
  25. self.can-install ?? $.prefix !! IO::Path;
  26. }
  27. method can-install() {
  28. $.prefix.w || ?(!$.prefix.e && try { $.prefix.mkdir } && $.prefix.e);
  29. }
  30. my $windows_wrapper = '@rem = \'--*-Perl-*--
  31. @echo off
  32. if "%OS%" == "Windows_NT" goto WinNT
  33. #perl# "%~dpn0" %1 %2 %3 %4 %5 %6 %7 %8 %9
  34. goto endofperl
  35. :WinNT
  36. #perl# "%~dpn0" %*
  37. if NOT "%COMSPEC%" == "%SystemRoot%\system32\cmd.exe" goto endofperl
  38. if %errorlevel% == 9009 echo You do not have Perl in your PATH.
  39. if errorlevel 1 goto script_failed_so_exit_with_non_zero_val 2>nul
  40. goto endofperl
  41. @rem \';
  42. __END__
  43. :endofperl
  44. ';
  45. my $perl_wrapper = '#!/usr/bin/env #perl#
  46. sub MAIN(:$name is copy, :$auth, :$ver, *@, *%) {
  47. shift @*ARGS if $name;
  48. shift @*ARGS if $auth;
  49. shift @*ARGS if $ver;
  50. $name //= \'#dist-name#\';
  51. my @installations = $*REPO.repo-chain.grep(CompUnit::Repository::Installable);
  52. my @binaries = flat @installations.map: { .files(\'bin/#name#\', :$name, :$auth, :$ver) };
  53. unless +@binaries {
  54. @binaries = flat @installations.map: { .files(\'bin/#name#\', :$name) };
  55. if +@binaries {
  56. note q:to/SORRY/;
  57. ===SORRY!===
  58. No candidate found for \'#name#\' that match your criteria.
  59. Did you perhaps mean one of these?
  60. SORRY
  61. my %caps = :name([\'Distribution\', 12]), :auth([\'Author(ity)\', 11]), :ver([\'Version\', 7]);
  62. for @binaries -> $dist {
  63. for %caps.kv -> $caption, @opts {
  64. @opts[1] = max @opts[1], ($dist{$caption} // \'\').Str.chars
  65. }
  66. }
  67. note \' \' ~ %caps.values.map({ sprintf(\'%-*s\', .[1], .[0]) }).join(\' | \');
  68. for @binaries -> $dist {
  69. note \' \' ~ %caps.kv.map( -> $k, $v { sprintf(\'%-*s\', $v.[1], $dist{$k} // \'\') } ).join(\' | \')
  70. }
  71. }
  72. else {
  73. note "===SORRY!===\nNo candidate found for \'#name#\'.\n";
  74. }
  75. exit 1;
  76. }
  77. %*ENV<PERL6_PROGRAM_NAME> = $*PROGRAM-NAME;
  78. exit run($*EXECUTABLE, @binaries.sort(*<ver>).tail.hash.<files><bin/#name#>, @*ARGS).exitcode
  79. }';
  80. method !sources-dir() {
  81. my $sources = $.prefix.add('sources');
  82. $sources.mkdir unless $sources.e;
  83. $sources
  84. }
  85. method !resources-dir() {
  86. my $resources = $.prefix.add('resources');
  87. $resources.mkdir unless $resources.e;
  88. $resources
  89. }
  90. method !dist-dir() {
  91. my $dist = $.prefix.add('dist');
  92. $dist.mkdir unless $dist.e;
  93. $dist
  94. }
  95. method !bin-dir() {
  96. my $bin = $.prefix.add('bin');
  97. $bin.mkdir unless $bin.e;
  98. $bin
  99. }
  100. method !add-short-name($name, $dist, $source?, $checksum?) {
  101. my $short-dir = $.prefix.add('short');
  102. my $id = nqp::sha1($name);
  103. my $lookup = $short-dir.add($id);
  104. $lookup.mkdir;
  105. $lookup.add($dist.id).spurt(
  106. "{$dist.meta<ver> // ''}\n"
  107. ~ "{$dist.meta<auth> // ''}\n"
  108. ~ "{$dist.meta<api> // ''}\n"
  109. ~ "{$source // ''}\n"
  110. ~ "{$checksum // ''}\n"
  111. );
  112. }
  113. method !remove-dist-from-short-name-lookup-files($dist --> Nil) {
  114. my $short-dir = $.prefix.add('short');
  115. return unless $short-dir.e;
  116. my $id = $dist.id;
  117. for $short-dir.dir -> $dir {
  118. $dir.add($id).unlink;
  119. $dir.rmdir unless $dir.dir;
  120. }
  121. }
  122. method !file-id(Str $name, Str $dist-id) {
  123. my $id = $name ~ $dist-id;
  124. nqp::sha1($id)
  125. }
  126. method name(--> Str:D) {
  127. CompUnit::RepositoryRegistry.name-for-repository(self)
  128. }
  129. method !repo-prefix() {
  130. my $repo-prefix = self.name // '';
  131. $repo-prefix ~= '#' if $repo-prefix;
  132. $repo-prefix
  133. }
  134. method !read-dist($id) {
  135. my $dist = Rakudo::Internals::JSON.from-json($.prefix.add('dist').add($id).slurp);
  136. $dist<ver> = $dist<ver> ?? Version.new( ~$dist<ver> ) !! Version.new('0');
  137. $dist
  138. }
  139. method !repository-version(--> Int:D) {
  140. return $!version if defined $!version;
  141. my $version-file = $.prefix.add('version');
  142. return $!version = 0 unless $version-file ~~ :f;
  143. $!version = $version-file.slurp.Int
  144. }
  145. method upgrade-repository() {
  146. my $version = self!repository-version;
  147. my $short-dir = $.prefix.add('short');
  148. mkdir $short-dir unless $short-dir.e;
  149. my $precomp-dir = $.prefix.add('precomp');
  150. mkdir $precomp-dir unless $precomp-dir.e;
  151. self!sources-dir;
  152. my $resources-dir = self!resources-dir;
  153. my $dist-dir = self!dist-dir;
  154. self!bin-dir;
  155. if ($version < 1) {
  156. for $short-dir.dir -> $file {
  157. my @ids = $file.lines.unique;
  158. $file.unlink;
  159. $file.mkdir;
  160. for @ids -> $id {
  161. my $dist = self!read-dist($id);
  162. $file.add($id).spurt("{$dist<ver> // ''}\n{$dist<auth> // ''}\n{$dist<api> // ''}\n");
  163. }
  164. }
  165. }
  166. if ($version < 2) {
  167. for $dist-dir.dir -> $dist-file {
  168. my %meta = Rakudo::Internals::JSON.from-json($dist-file.slurp);
  169. my $files = %meta<files> //= [];
  170. for eager $files.keys -> $file {
  171. $files{"resources/$file"} = $files{$file}:delete
  172. if $resources-dir.add($files{$file}).e
  173. and not $.prefix.add($file).e; # bin/ is already included in the path
  174. }
  175. $dist-file.spurt: Rakudo::Internals::JSON.to-json(%meta);
  176. }
  177. }
  178. $.prefix.add('version').spurt('2');
  179. $!version = 2;
  180. }
  181. proto method install(|) {*}
  182. multi method install($dist, %sources, %scripts?, %resources?, Bool :$force) {
  183. # XXX: Deprecation shim
  184. my %files;
  185. %files{"bin/$_.key()"} = $_.value for %scripts.pairs;
  186. %files{"resources/$_.key()"} = $_.value for %resources.pairs;
  187. my %meta6 = %(
  188. name => $dist.?name,
  189. ver => $dist.?ver // $dist.?version,
  190. auth => $dist.?auth // $dist.?authority,
  191. provides => %sources,
  192. files => %files,
  193. );
  194. return samewith(Distribution::Hash.new(%meta6, :prefix($*CWD)), :$force);
  195. }
  196. multi method install(Distribution $distribution, Bool :$force) {
  197. my $dist = CompUnit::Repository::Distribution.new($distribution);
  198. my %files = $dist.meta<files>.grep(*.defined).map: -> $link {
  199. $link ~~ Str ?? ($link => $link) !! ($link.keys[0] => $link.values[0])
  200. }
  201. $!lock.protect( {
  202. my @*MODULES;
  203. my $path = self!writeable-path or die "No writeable path found, $.prefix not writeable";
  204. my $lock = $.prefix.add('repo.lock').open(:create, :w);
  205. $lock.lock;
  206. my $version = self!repository-version;
  207. self.upgrade-repository unless $version == 2;
  208. my $dist-id = $dist.id;
  209. my $dist-dir = self!dist-dir;
  210. if not $force and $dist-dir.add($dist-id) ~~ :e {
  211. $lock.unlock;
  212. fail "$dist already installed";
  213. }
  214. my $sources-dir = self!sources-dir;
  215. my $resources-dir = self!resources-dir;
  216. my $bin-dir = self!bin-dir;
  217. my $is-win = Rakudo::Internals.IS-WIN;
  218. self!add-short-name($dist.meta<name>, $dist); # so scripts can find their dist
  219. my %links; # map name-path to new content address
  220. my %provides; # meta data gets added, but the format needs to change to
  221. # only extend the structure, not change it
  222. # the following 3 `for` loops should be a single loop, but has been
  223. # left this way due to impeding precomp changes
  224. # lib/ source files
  225. for $dist.meta<provides>.kv -> $name, $file is copy {
  226. # $name is "Inline::Perl5" while $file is "lib/Inline/Perl5.pm6"
  227. my $id = self!file-id(~$name, $dist-id);
  228. my $destination = $sources-dir.add($id);
  229. my $handle = $dist.content($file);
  230. self!add-short-name($name, $dist, $id, nqp::sha1($handle.open(:enc<iso-8859-1>).slurp(:close)));
  231. %provides{ $name } = ~$file => {
  232. :file($id),
  233. :time(try $file.IO.modified.Num),
  234. :$!cver
  235. };
  236. note("Installing {$name} for {$dist.meta<name>}") if $verbose and $name ne $dist.meta<name>;
  237. my $content = $handle.open.slurp-rest(:bin,:close);
  238. $destination.spurt($content);
  239. $handle.close;
  240. }
  241. # bin/ scripts
  242. for %files.kv -> $name-path, $file is copy {
  243. next unless $name-path.starts-with('bin/');
  244. my $id = self!file-id(~$file, $dist-id);
  245. my $destination = $resources-dir.add($id); # wrappers are put in bin/; originals in resources/
  246. my $withoutext = $name-path.subst(/\.[exe|bat]$/, '');
  247. for '', '-j', '-m' -> $be {
  248. $.prefix.add("$withoutext$be").IO.spurt:
  249. $perl_wrapper.subst('#name#', $name-path.IO.basename, :g).subst('#perl#', "perl6$be").subst('#dist-name#', $dist.meta<name>);
  250. if $is-win {
  251. $.prefix.add("$withoutext$be.bat").IO.spurt:
  252. $windows_wrapper.subst('#perl#', "perl6$be", :g);
  253. }
  254. else {
  255. $.prefix.add("$withoutext$be").IO.chmod(0o755);
  256. }
  257. }
  258. self!add-short-name($name-path, $dist);
  259. %links{$name-path} = $id;
  260. my $handle = $dist.content($file);
  261. my $content = $handle.open.slurp-rest(:bin,:close);
  262. $destination.spurt($content);
  263. $handle.close;
  264. }
  265. # resources/
  266. for %files.kv -> $name-path, $file is copy {
  267. next unless $name-path.starts-with('resources/');
  268. # $name-path is 'resources/libraries/p5helper' while $file is 'resources/libraries/libp5helper.so'
  269. my $id = self!file-id(~$name-path, $dist-id) ~ '.' ~ $file.IO.extension;
  270. my $destination = $resources-dir.add($id);
  271. %links{$name-path} = $id;
  272. my $handle = $dist.content($file);
  273. my $content = $handle.open.slurp-rest(:bin,:close);
  274. $destination.spurt($content);
  275. $handle.close;
  276. }
  277. my %meta = %($dist.meta);
  278. %meta<files> = %links; # add our new name-path => conent-id mapping
  279. %meta<provides> = %provides; # new meta data added to provides
  280. %!dist-metas{$dist-id} = %meta;
  281. $dist-dir.add($dist-id).spurt: Rakudo::Internals::JSON.to-json(%meta);
  282. # reset cached id so it's generated again on next access.
  283. # identity changes with every installation of a dist.
  284. $!id = Any;
  285. {
  286. my $head = $*REPO;
  287. PROCESS::<$REPO> := self; # Precomp files should only depend on downstream repos
  288. my $precomp = $*REPO.precomp-repository;
  289. my $repo-prefix = self!repo-prefix;
  290. my $*RESOURCES = Distribution::Resources.new(:repo(self), :$dist-id);
  291. my %done;
  292. my $compiler-id = CompUnit::PrecompilationId.new($*PERL.compiler.id);
  293. for %provides.kv -> $source-name, $source-meta {
  294. my $id = CompUnit::PrecompilationId.new($source-meta.values[0]<file>);
  295. $precomp.store.delete($compiler-id, $id);
  296. }
  297. for %provides.kv -> $source-name, $source-meta {
  298. my $id = $source-meta.values[0]<file>;
  299. my $source = $sources-dir.add($id);
  300. my $source-file = $repo-prefix ?? $repo-prefix ~ $source.relative($.prefix) !! $source;
  301. if %done{$id} {
  302. note "(Already did $id)" if $verbose;
  303. next;
  304. }
  305. note("Precompiling $id ($source-name)") if $verbose;
  306. $precomp.precompile(
  307. $source.IO,
  308. CompUnit::PrecompilationId.new($id),
  309. :source-name("$source-file ($source-name)"),
  310. );
  311. %done{$id} = 1;
  312. }
  313. PROCESS::<$REPO> := $head;
  314. }
  315. $lock.unlock;
  316. } ) }
  317. method uninstall(Distribution $distribution) {
  318. my $repo-version = self!repository-version;
  319. self.upgrade-repository unless $repo-version == 2;
  320. # xxx: currently needs to be passed in a distribution object that
  321. # has meta<files> pointing at content-ids, so you cannot yet just
  322. # pass in the original meta data and have it discovered and deleted
  323. # (i.e. update resolve to return such a ::Installation::Distribution)
  324. my $dist = CompUnit::Repository::Distribution.new($distribution);
  325. my %provides = $dist.meta<provides>;
  326. my %files = $dist.meta<files>;
  327. my $sources-dir = self.prefix.add('sources');
  328. my $resources-dir = self.prefix.add('resources');
  329. my $bin-dir = self.prefix.add('bin');
  330. my $dist-dir = self.prefix.add('dist');
  331. self!remove-dist-from-short-name-lookup-files($dist);
  332. my sub unlink-if-exists($path) { unlink($path) if $path.IO.e }
  333. # delete special directory files
  334. for %files.kv -> $name-path, $file {
  335. given $name-path {
  336. when /^bin\/(.*)/ {
  337. # wrappers are located in $bin-dir (only delete if no other versions use wrapper)
  338. unless self.files($name-path, :name($dist.meta<name>)).elems {
  339. unlink-if-exists( $bin-dir.add("$0$_") ) for '', '-m', '-j';
  340. }
  341. # original bin scripts are in $resources-dir
  342. unlink-if-exists( $resources-dir.add($file) )
  343. }
  344. when /^resources\// {
  345. unlink-if-exists( $resources-dir.add($file) )
  346. }
  347. }
  348. }
  349. # delete sources
  350. unlink-if-exists( $sources-dir.add($_) ) for %provides.values.flatmap(*.values.map(*.<file>));
  351. # delete the meta file
  352. unlink( $dist-dir.add($dist.id) )
  353. }
  354. method files($file, :$name!, :$auth, :$ver) {
  355. my @candi;
  356. my $prefix = self.prefix;
  357. my $lookup = $prefix.add('short').add(nqp::sha1($name));
  358. if $lookup.e {
  359. my $repo-version = self!repository-version;
  360. my @dists = $repo-version < 1
  361. ?? $lookup.lines.unique.map({
  362. self!read-dist($_)
  363. })
  364. !! $lookup.dir.map({
  365. my ($ver, $auth, $api) = $_.slurp.split("\n");
  366. (id => $_.basename, ver => Version.new( $ver || 0 ), auth => $auth, api => $api).hash
  367. });
  368. for @dists.grep({$_<auth> ~~ $auth and $_<ver> ~~ $ver}) -> $dist is copy {
  369. $dist = self!read-dist($dist<id>) if $repo-version >= 1;
  370. with $dist<files>{$file} {
  371. my $candi = %$dist;
  372. $candi<files>{$file} = self!resources-dir.add($candi<files>{$file});
  373. @candi.push: $candi;
  374. }
  375. }
  376. }
  377. @candi
  378. }
  379. method !matching-dist(CompUnit::DependencySpecification $spec) {
  380. if $spec.from eq 'Perl6' {
  381. my $repo-version = self!repository-version;
  382. my $lookup = $.prefix.add('short').add(nqp::sha1($spec.short-name));
  383. if $lookup.e {
  384. my @dists = (
  385. $repo-version < 1
  386. ?? $lookup.lines.unique.map({
  387. $_ => self!read-dist($_)
  388. })
  389. !! $lookup.dir.map({
  390. my ($ver, $auth, $api, $source, $checksum) = $_.slurp.split("\n");
  391. $_.basename => {
  392. ver => Version.new( $ver || 0 ),
  393. auth => $auth,
  394. api => $api,
  395. source => $source || Any,
  396. checksum => $checksum || Str,
  397. }
  398. })
  399. ).grep({
  400. $_.value<auth> ~~ $spec.auth-matcher
  401. and $_.value<ver> ~~ $spec.version-matcher
  402. });
  403. for @dists.sort(*.value<ver>).reverse.map(*.kv) -> ($dist-id, $dist) {
  404. return ($dist-id, $dist);
  405. }
  406. }
  407. }
  408. Nil
  409. }
  410. method !lazy-distribution($dist-id) {
  411. class :: does Distribution::Locally {
  412. has $.dist-id;
  413. has $.read-dist;
  414. has $!installed-dist;
  415. method !dist {
  416. $!installed-dist //= InstalledDistribution.new($.read-dist()(), :$.prefix)
  417. }
  418. method meta(--> Hash:D) { self!dist.meta }
  419. method content($content-id --> IO::Handle:D) { self!dist.content($content-id) }
  420. method Str() { self!dist.Str }
  421. }.new(
  422. :$dist-id,
  423. :read-dist(-> { self!read-dist($dist-id) })
  424. :$.prefix,
  425. )
  426. }
  427. method resolve(
  428. CompUnit::DependencySpecification $spec,
  429. --> CompUnit:D)
  430. {
  431. my ($dist-id, $dist) = self!matching-dist($spec);
  432. if $dist-id {
  433. # xxx: replace :distribution with meta6
  434. return CompUnit.new(
  435. :handle(CompUnit::Handle),
  436. :short-name($spec.short-name),
  437. :version($dist<ver>),
  438. :auth($dist<auth> // Str),
  439. :repo(self),
  440. :repo-id($dist<source> // self!read-dist($dist-id)<provides>{$spec.short-name}.values[0]<file>),
  441. :distribution(self!lazy-distribution($dist-id)),
  442. );
  443. }
  444. return self.next-repo.resolve($spec) if self.next-repo;
  445. Nil
  446. }
  447. method !precomp-stores() {
  448. $!precomp-stores //= Array[CompUnit::PrecompilationStore].new(
  449. self.repo-chain.map(*.precomp-store).grep(*.defined)
  450. )
  451. }
  452. method need(
  453. CompUnit::DependencySpecification $spec,
  454. CompUnit::PrecompilationRepository $precomp = self.precomp-repository(),
  455. CompUnit::PrecompilationStore :@precomp-stores = self!precomp-stores(),
  456. --> CompUnit:D)
  457. {
  458. my ($dist-id, $dist) = self!matching-dist($spec);
  459. if $dist-id {
  460. return %!loaded{~$spec} if %!loaded{~$spec}:exists;
  461. my $source-file-name = $dist<source>
  462. // do {
  463. my $provides = self!read-dist($dist-id)<provides>;
  464. X::CompUnit::UnsatisfiedDependency.new(:specification($spec)).throw
  465. unless $provides{$spec.short-name}:exists;
  466. $provides{$spec.short-name}.values[0]<file>
  467. };
  468. my $loader = $.prefix.add('sources').add($source-file-name);
  469. my $*RESOURCES = Distribution::Resources.new(:repo(self), :$dist-id);
  470. my $id = $loader.basename;
  471. my $repo-prefix = self!repo-prefix;
  472. my $handle = $precomp.try-load(
  473. CompUnit::PrecompilationDependency::File.new(
  474. :id(CompUnit::PrecompilationId.new($id)),
  475. :src($repo-prefix ?? $repo-prefix ~ $loader.relative($.prefix) !! $loader.absolute),
  476. :checksum($dist<checksum>:exists ?? $dist<checksum> !! Str),
  477. :$spec,
  478. ),
  479. :source($loader),
  480. :@precomp-stores,
  481. );
  482. my $precompiled = defined $handle;
  483. $handle //= CompUnit::Loader.load-source-file($loader);
  484. # xxx: replace :distribution with meta6
  485. my $compunit = CompUnit.new(
  486. :$handle,
  487. :short-name($spec.short-name),
  488. :version($dist<ver>),
  489. :auth($dist<auth> // Str),
  490. :repo(self),
  491. :repo-id($id),
  492. :$precompiled,
  493. :distribution(self!lazy-distribution($dist-id)),
  494. );
  495. return %!loaded{~$spec} = $compunit;
  496. }
  497. return self.next-repo.need($spec, $precomp, :@precomp-stores) if self.next-repo;
  498. X::CompUnit::UnsatisfiedDependency.new(:specification($spec)).throw;
  499. }
  500. method resource($dist-id, $key) {
  501. my $dist = %!dist-metas{$dist-id} //= Rakudo::Internals::JSON.from-json(self!dist-dir.add($dist-id).slurp);
  502. # need to strip the leading resources/ on old repositories
  503. self!resources-dir.add($dist<files>{$key.substr(self!repository-version < 2 ?? 10 !! 0)})
  504. }
  505. method id() {
  506. return $!id if $!id;
  507. my $name = self.path-spec;
  508. $name ~= ',' ~ self.next-repo.id if self.next-repo;
  509. my $dist-dir = $.prefix.add('dist');
  510. $!id = nqp::sha1(nqp::sha1($name) ~ ($dist-dir.e ?? $dist-dir.dir !! ''))
  511. }
  512. method short-id() { 'inst' }
  513. method loaded(--> Iterable:D) {
  514. return %!loaded.values;
  515. }
  516. method distribution($id) {
  517. InstalledDistribution.new(self!read-dist($id), :prefix(self.prefix))
  518. }
  519. method installed(--> Iterable:D) {
  520. my $dist-dir = self.prefix.add('dist');
  521. $dist-dir.e
  522. ?? $dist-dir.dir.map({ self.distribution($_.basename) })
  523. !! Nil
  524. }
  525. method precomp-store(--> CompUnit::PrecompilationStore:D) {
  526. $!precomp-store //= CompUnit::PrecompilationStore::File.new(
  527. :prefix(self.prefix.add('precomp')),
  528. )
  529. }
  530. method precomp-repository(--> CompUnit::PrecompilationRepository:D) {
  531. $!precomp := CompUnit::PrecompilationRepository::Default.new(
  532. :store(self.precomp-store),
  533. ) unless $!precomp;
  534. $!precomp
  535. }
  536. sub provides-warning($is-win, $name --> Nil) {
  537. my ($red,$clear) = Rakudo::Internals.error-rcgye;
  538. note "$red==={$clear}WARNING!$red===$clear
  539. The distribution $name does not seem to have a \"provides\" section in its META.info file,
  540. and so the packages will not be installed in the correct location.
  541. Please ask the author to add a \"provides\" section, mapping every exposed namespace to a
  542. file location in the distribution.
  543. See http://design.perl6.org/S22.html#provides for more information.\n";
  544. }
  545. }