diff options
65 files changed, 4080 insertions, 0 deletions
@@ -0,0 +1,248 @@ +Revision history for carton + +v1.0.21 2015-05-15 12:21:47 PDT + - Changed Module::Reader dependency to requires for now, since it will be required + on the runtime by the fatpacked carton. + +v1.0.20 2015-05-08 16:20:37 PDT + - INCOMPATIBLE: Disable fatpack generation in carton bundle by default. + Use the new standalone command carton fatpack, to generate vendor/bin/carton + +v1.0.19 2015-05-01 17:48:06 PDT + - Remove Module::Build and MakeMaker from prerequisite since cpanm will + install them as required + +v1.0.18 2015-04-29 13:46:21 PDT + - Sort 02packages case insensitive, like PAUSE + +v1.0.17 2015-04-27 16:18:04 PDT + - Add back warnings to Carton::CLI + - Properly fail when a command is not found in carton exec (hachi) #193 + +v1.0.16 2015-04-25 06:37:17 PDT + - update 02packages.details.txt whitespace padding to follow PAUSE + +v1.0.15 2015-04-20 11:13:32 CEST + - downgrade some dependencies for fatpack-related tools to recommends + (probably ship it as a separate distribution in the future) + +v1.0.14 2015-04-20 00:07:26 CEST + - same as v1.0.14 + +v1.0.13 2015-04-19 19:38:42 CEST + - require cpanm 1.7030 for better cpanfile support + - switch to MakeMaker + - remove Exception::Class and Moo in favor of Class::Tiny + - add an ability to set generator in Carton::Index for Carmel use + +v1.0.12 2013-09-24 20:03:47 JST + - up Path::Tiny + - Add --no-fatpack to carton bundle #140 + +v1.0.11 2013-09-18 18:51:14 JST + - Disable fatal warnings that comes with Moo. This will make Path::Tiny not fail + on NFS without flock. #135 + +v1.0.10 2013-09-02 17:52:42 PDT + - Documentation fixes + - Support CRLF in snapshot (kan) #133 + +v1.0.9 2013-08-17 11:24:46 PDT + - Workaround carton help shows wrong doc on case insensitive filesystems + +v1.0.8 2013-08-16 18:38:14 PDT + - Include POD documentation for carton(1) + +v1.0.7 2013-08-10 21:55:29 PDT + - Worked around fatpack issue with perl < 5.16 with missing File::Spec + - Included missing requirements for fatpack executable + +v1.0.6 2013-08-10 17:59:51 PDT + - Added upgrading documentation (carton help upgrading) + - Experimental support for fatpacked carton executable in vendor/bin in bundle + +v1.0.5 2013-08-08 12:50:39 PDT + - Bump cpanm for version extraction #126 + - Fix doc about --cached (shibayu36) + - Fix Usage errors #123 + +v1.0.4 2013-08-05 19:20:11 PDT + - Bump cpanm to deal with failing to extract versions with version.pm #120 (lestrrat) + +v1.0.3 2013-08-05 10:17:59 PDT + - Update obsolete docs + - Added missing docs to some commands + +v1.0.2 2013-08-04 21:49:14 PDT + - Bump cpanm dependency to deal with old dists with ancient META.yml (tokuhirom) + +v1.0.1 2013-08-04 17:10:10 PDT + - Update docs + - Fixed bug where version range requirements are not properly preserved in the snapshot #117 (lestrrat) + +v1.0.0 2013-08-04 12:29:31 PDT + - This makes 1.0 release + - Documentation update + - Bump cpanm dependency + +v0.9.68 2013-07-26 17:49:28 PDT + - Change the distribution name case, to match with the module name + - Add Module::CoreList as a dependency for perl 5.8 + +v0.9.67 2013-07-24 14:53:53 PDT + - Use cpanm's fatscript interface rather than share dir + +v0.9.66 2013-07-24 08:46:27 PDT + - Correctly raises an exception when badly formatted snapshot file is read + - Fixed a bug in tree scanner where seen hash is not preserved + - tree scanner should be much faster for giant set of dependencies + +v0.9.65 2013-07-23 18:51:59 PDT + - BIG CHANGE: Use cpanfile.snapshot instead of carton.lock for 1.0 onwards + There is no way to migrate carton.lock to cpanfile.snapshot just yet. + - New cpanfile.snapshot is text based and is more VCS friendly + - Reworked internal of prereqs/requirements loader (again!) to eliminate + lots of duplicate code + +v0.9.64 2013-07-23 14:39:54 PDT + - Locate cpanm within a dist share dir so that cpanm in user's $PATH is not executed. + This will solve issues with cpanm in /usr/bin, perlbrew or with bad shebang #92 + +v0.9.63 2013-07-23 02:26:04 PDT + - Bump cpanminus requirement + - Support --cpanfile for carton install + - Support PERL_CARTON_CPANFILE (for commands other than install) + +v0.9.62 2013-07-22 14:33:11 PDT + - Now all carton commands can be run from a subdirectory #69 + - Refactored the way cpanfile/carton.lock files are detected + +v0.9.61 2013-07-22 10:24:35 PDT + - Implemented experimental --without option for carton install + +v0.9.60 2013-06-26 12:22:21 PDT + - Bump MakeMaker and Module::Build dependencies to support test requires + +v0.9.59 2013-06-17 17:13:21 PDT + - carton exec -Ilib gives a warning, while carton exec perl -Ilib won't #97 + +v0.9.58 2013-06-10 03:17:23 PDT + - Fix tests + - carton exec without an arg should raise an error + - typo fixes + +v0.9.57 2013-06-05 19:21:17 JST + - Changed the output of carton tree command to include module and dist versions + - Bunch of refactorings around requirements + - carton install now saves tarballs in local/cache, then carton bundle copies from there + - Implement carton check which checks if cpanfile requirements is satisfied locally with lock + - Implement carton update! + - Fix the installation collector logic to ignore dists that don't satisfy cpanfile + +v0.9.56 2013-06-04 00:21:53 JST + - Fixed carton tree output to avoid duplicates + +v0.9.55 2013-06-03 23:43:52 JST + - Added back carton tree command + - Added --distfile option to list command + +v0.9.54 2013-06-02 12:38:20 JST + - Install develop phase dependencies by default with carton install + - carton exec now doesn't set PERL5OPT with lib::core::only, so as not to mess with + the subprocess and site_perl modules (#60, #70, #82) + +v0.9.53 2013-06-01 23:54:53 JST + - use Moo + - refactored installer/downloader as Carton::Builder + +v0.9.52 2013-06-01 17:01:51 JST + - carton exec doesn't need '--' before perl anymore #77 + - remove even more unused code + - backed out color output support + - stopped collecting dependencies from cpanfile, since cpanm installdeps can read it directly + - Temporarily disabled check command for now + - Upped cpanm dependency + - Use vendor/cache for bundling since local is most likely gitignored #88 + - carton exec now requires carton install beforehand + +v0.9.51 2013-05-31 09:02:58 JST + - Documentation fixes + - Fixes test dependencies and build system + +v0.9.50 2013-05-31 02:18:07 JST + - Documentation fixes + - remove bunch of code that is unused + - removed tree command for now + - Overhauled the way bundle command works + - refactored lock and index generation code + - Enabled Travis CI tests + +v0.9.15 2013-03-31 18:11:28 PDT + - Add minimum perl dependency + +v0.9.14 2013-03-30 18:25:39 PDT + - Unset $VERSION on PAUSE (Thanks andk) + +v0.9.13 2013-03-30 15:14:49 PDT + - repackage for better META files with Milla v0.9.3 + +v0.9.12 2013-03-30 15:01:55 PDT + - repackage to set $VERSION + +v0.9.11 2013-03-30 14:54:21 PDT + - Ignore 'perl' requirements so as it won't fail, for now. #71 + - Install 'test' dependencies by default. #66 + - Convert to Milla, do not install carton-* man pages + +v0.9.10 Tue Feb 26 13:32:34 PST 2013 + - Same as v0.9_9. Still considered pre-1.0! + +v0.9_9 Wed Feb 6 11:02:46 PST 2013 + - Fixed bundle command where it updated modules, not the versions specified in carton.lock. + bundle now builds mirror files like install --deployment, and downloads tarballs for the + specified versions. (vti) + +v0.9_8 Tue Feb 5 12:17:54 PST 2013 + - Do not use carton.lock to build extra dependencies. Everything has to be + pulled out of cpanfile, even with the deployment mode. This makes the deployment + much more reliable, and could possibly work with differing os/perl versions + across development and deployments. + +v0.9_7 Sat May 12 06:15:44 EEST 2012 + - Experimental multiple mirror support (nihen) + - Fixed cpanm dependency to avoid cascading bug + +v0.9_6 Thu May 10 21:05:35 CEST 2012 + - use cpanfile + Module::Install for dogfooding + - `carton` without args now does `carton install` (inspired by bundler) + - Update bundle command to use install.json (masaki) + - code cleanups and doc overhauls + - removed `uninstall` command for now + - Fixed CPAN::Meta::Requirements dependency + +v0.9_5 Thu Apr 12 19:39:19 JST 2012 + - Added experimental cpanfile support + - Fixed POD (yanick) + +v0.9.4 Sat Mar 31 13:49:41 CEST 2012 + - use Capture::Tiny to capture output (wchristian) + - Improve synopsis for exec (dagolden) + - Implemented bundle command (masaki) + - Fix Getopt::Long dependency (pfig) + +v0.9.3 Wed Oct 19 14:30:50 JST 2011 + - Fixed META.yml by patching Module::Install and repackaging + +v0.9.2 Tue Oct 18 12:53:57 JST 2011 + - Fixed packaging *again* by declaring version as a simple string + via http://www.dagolden.com/index.php/369/version-numbers-should-be-boring/ + +v0.9.1 Mon Oct 17 19:05:12 JST 2011 + - Fixed packaging + - Fixed UTF8 encoding warnings for JSON + +v0.9.0 Fri Oct 14 01:27:02 JST 2011 + - Initial non-dev release. Still considered beta before it hits 1.0.0! + +v0.1_0 Sun Jun 26 11:03:50 PDT 2011 + - original version @@ -0,0 +1,379 @@ +This software is copyright (c) 2011- by Tatsuhiko Miyagawa. + +This is free software; you can redistribute it and/or modify it under +the same terms as the Perl 5 programming language system itself. + +Terms of the Perl programming language system itself + +a) the GNU General Public License as published by the Free + Software Foundation; either version 1, or (at your option) any + later version, or +b) the "Artistic License" + +--- The GNU General Public License, Version 1, February 1989 --- + +This software is Copyright (c) 2011- by Tatsuhiko Miyagawa. + +This is free software, licensed under: + + The GNU General Public License, Version 1, February 1989 + + GNU GENERAL PUBLIC LICENSE + Version 1, February 1989 + + Copyright (C) 1989 Free Software Foundation, Inc. + 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The license agreements of most software companies try to keep users +at the mercy of those companies. By contrast, our General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. The +General Public License applies to the Free Software Foundation's +software and to any other program whose authors commit to using it. +You can use it for your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Specifically, the General Public License is designed to make +sure that you have the freedom to give away or sell copies of free +software, that you receive source code or can get it if you want it, +that you can change the software or use pieces of it in new free +programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of a such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must tell them their rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License Agreement applies to any program or other work which +contains a notice placed by the copyright holder saying it may be +distributed under the terms of this General Public License. The +"Program", below, refers to any such program or work, and a "work based +on the Program" means either the Program or any work containing the +Program or a portion of it, either verbatim or with modifications. Each +licensee is addressed as "you". + + 1. You may copy and distribute verbatim copies of the Program's source +code as you receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice and +disclaimer of warranty; keep intact all the notices that refer to this +General Public License and to the absence of any warranty; and give any +other recipients of the Program a copy of this General Public License +along with the Program. You may charge a fee for the physical act of +transferring a copy. + + 2. You may modify your copy or copies of the Program or any portion of +it, and copy and distribute such modifications under the terms of Paragraph +1 above, provided that you also do the following: + + a) cause the modified files to carry prominent notices stating that + you changed the files and the date of any change; and + + b) cause the whole of any work that you distribute or publish, that + in whole or in part contains the Program or any part thereof, either + with or without modifications, to be licensed at no charge to all + third parties under the terms of this General Public License (except + that you may choose to grant warranty protection to some or all + third parties, at your option). + + c) If the modified program normally reads commands interactively when + run, you must cause it, when started running for such interactive use + in the simplest and most usual way, to print or display an + announcement including an appropriate copyright notice and a notice + that there is no warranty (or else, saying that you provide a + warranty) and that users may redistribute the program under these + conditions, and telling the user how to view a copy of this General + Public License. + + d) You may charge a fee for the physical act of transferring a + copy, and you may at your option offer warranty protection in + exchange for a fee. + +Mere aggregation of another independent work with the Program (or its +derivative) on a volume of a storage or distribution medium does not bring +the other work under the scope of these terms. + + 3. You may copy and distribute the Program (or a portion or derivative of +it, under Paragraph 2) in object code or executable form under the terms of +Paragraphs 1 and 2 above provided that you also do one of the following: + + a) accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of + Paragraphs 1 and 2 above; or, + + b) accompany it with a written offer, valid for at least three + years, to give any third party free (except for a nominal charge + for the cost of distribution) a complete machine-readable copy of the + corresponding source code, to be distributed under the terms of + Paragraphs 1 and 2 above; or, + + c) accompany it with the information you received as to where the + corresponding source code may be obtained. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form alone.) + +Source code for a work means the preferred form of the work for making +modifications to it. For an executable file, complete source code means +all the source code for all modules it contains; but, as a special +exception, it need not include source code for modules which are standard +libraries that accompany the operating system on which the executable +file runs, or for standard header files or definitions files that +accompany that operating system. + + 4. You may not copy, modify, sublicense, distribute or transfer the +Program except as expressly provided under this General Public License. +Any attempt otherwise to copy, modify, sublicense, distribute or transfer +the Program is void, and will automatically terminate your rights to use +the Program under this License. However, parties who have received +copies, or rights to use copies, from you under this General Public +License will not have their licenses terminated so long as such parties +remain in full compliance. + + 5. By copying, distributing or modifying the Program (or any work based +on the Program) you indicate your acceptance of this license to do so, +and all its terms and conditions. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the original +licensor to copy, distribute or modify the Program subject to these +terms and conditions. You may not impose any further restrictions on the +recipients' exercise of the rights granted herein. + + 7. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of the license which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +the license, you may choose any version ever published by the Free Software +Foundation. + + 8. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 9. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 10. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + Appendix: How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to humanity, the best way to achieve this is to make it +free software which everyone can redistribute and change under these +terms. + + To do so, attach the following notices to the program. It is safest to +attach them to the start of each source file to most effectively convey +the exclusion of warranty; and each file should have at least the +"copyright" line and a pointer to where the full notice is found. + + <one line to give the program's name and a brief idea of what it does.> + Copyright (C) 19yy <name of author> + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 1, or (at your option) + any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301 USA + + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) 19xx name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the +appropriate parts of the General Public License. Of course, the +commands you use may be called something other than `show w' and `show +c'; they could even be mouse-clicks or menu items--whatever suits your +program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the + program `Gnomovision' (a program to direct compilers to make passes + at assemblers) written by James Hacker. + + <signature of Ty Coon>, 1 April 1989 + Ty Coon, President of Vice + +That's all there is to it! + + +--- The Artistic License 1.0 --- + +This software is Copyright (c) 2011- by Tatsuhiko Miyagawa. + +This is free software, licensed under: + + The Artistic License 1.0 + +The Artistic License + +Preamble + +The intent of this document is to state the conditions under which a Package +may be copied, such that the Copyright Holder maintains some semblance of +artistic control over the development of the package, while giving the users of +the package the right to use and distribute the Package in a more-or-less +customary fashion, plus the right to make reasonable modifications. + +Definitions: + + - "Package" refers to the collection of files distributed by the Copyright + Holder, and derivatives of that collection of files created through + textual modification. + - "Standard Version" refers to such a Package if it has not been modified, + or has been modified in accordance with the wishes of the Copyright + Holder. + - "Copyright Holder" is whoever is named in the copyright or copyrights for + the package. + - "You" is you, if you're thinking about copying or distributing this Package. + - "Reasonable copying fee" is whatever you can justify on the basis of media + cost, duplication charges, time of people involved, and so on. (You will + not be required to justify it to the Copyright Holder, but only to the + computing community at large as a market that must bear the fee.) + - "Freely Available" means that no fee is charged for the item itself, though + there may be fees involved in handling the item. It also means that + recipients of the item may redistribute it under the same conditions they + received it. + +1. You may make and give away verbatim copies of the source form of the +Standard Version of this Package without restriction, provided that you +duplicate all of the original copyright notices and associated disclaimers. + +2. You may apply bug fixes, portability fixes and other modifications derived +from the Public Domain or from the Copyright Holder. A Package modified in such +a way shall still be considered the Standard Version. + +3. You may otherwise modify your copy of this Package in any way, provided that +you insert a prominent notice in each changed file stating how and when you +changed that file, and provided that you do at least ONE of the following: + + a) place your modifications in the Public Domain or otherwise make them + Freely Available, such as by posting said modifications to Usenet or an + equivalent medium, or placing the modifications on a major archive site + such as ftp.uu.net, or by allowing the Copyright Holder to include your + modifications in the Standard Version of the Package. + + b) use the modified Package only within your corporation or organization. + + c) rename any non-standard executables so the names do not conflict with + standard executables, which must also be provided, and provide a separate + manual page for each non-standard executable that clearly documents how it + differs from the Standard Version. + + d) make other distribution arrangements with the Copyright Holder. + +4. You may distribute the programs of this Package in object code or executable +form, provided that you do at least ONE of the following: + + a) distribute a Standard Version of the executables and library files, + together with instructions (in the manual page or equivalent) on where to + get the Standard Version. + + b) accompany the distribution with the machine-readable source of the Package + with your modifications. + + c) accompany any non-standard executables with their corresponding Standard + Version executables, giving the non-standard executables non-standard + names, and clearly documenting the differences in manual pages (or + equivalent), together with instructions on where to get the Standard + Version. + + d) make other distribution arrangements with the Copyright Holder. + +5. You may charge a reasonable copying fee for any distribution of this +Package. You may charge any fee you choose for support of this Package. You +may not charge a fee for this Package itself. However, you may distribute this +Package in aggregate with other (possibly commercial) programs as part of a +larger (possibly commercial) software distribution provided that you do not +advertise this Package as a product of your own. + +6. The scripts and library files supplied as input to or produced as output +from the programs of this Package do not automatically fall under the copyright +of this Package, but belong to whomever generated them, and may be sold +commercially, and may be aggregated with this Package. + +7. C or perl subroutines supplied by you and linked into this Package shall not +be considered part of this Package. + +8. The name of the Copyright Holder may not be used to endorse or promote +products derived from this software without specific prior written permission. + +9. THIS PACKAGE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR IMPLIED +WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED WARRANTIES OF +MERCHANTIBILITY AND FITNESS FOR A PARTICULAR PURPOSE. + +The End + diff --git a/MANIFEST b/MANIFEST new file mode 100644 index 0000000..6791b35 --- /dev/null +++ b/MANIFEST @@ -0,0 +1,66 @@ +# This file was automatically generated by Dist::Zilla::Plugin::Manifest v5.036. +Changes +LICENSE +MANIFEST +META.json +META.yml +Makefile.PL +README +cpanfile +dist.ini +lib/Carton.pm +lib/Carton/Builder.pm +lib/Carton/CLI.pm +lib/Carton/CPANfile.pm +lib/Carton/Dependency.pm +lib/Carton/Dist.pm +lib/Carton/Dist/Core.pm +lib/Carton/Doc/Bundle.pod +lib/Carton/Doc/Check.pod +lib/Carton/Doc/Exec.pod +lib/Carton/Doc/FAQ.pod +lib/Carton/Doc/Fatpack.pod +lib/Carton/Doc/Install.pod +lib/Carton/Doc/List.pod +lib/Carton/Doc/Show.pod +lib/Carton/Doc/Tree.pod +lib/Carton/Doc/Update.pod +lib/Carton/Doc/Upgrading.pod +lib/Carton/Doc/Version.pod +lib/Carton/Environment.pm +lib/Carton/Error.pm +lib/Carton/Index.pm +lib/Carton/Mirror.pm +lib/Carton/Package.pm +lib/Carton/Packer.pm +lib/Carton/Snapshot.pm +lib/Carton/Snapshot/Emitter.pm +lib/Carton/Snapshot/Parser.pm +lib/Carton/Tree.pm +lib/Carton/Util.pm +script/carton +t/release-pod-syntax.t +xt/CLI.pm +xt/cli/bundle.t +xt/cli/check.t +xt/cli/cpanfile.t +xt/cli/deployment.t +xt/cli/deps_phase.t +xt/cli/exec.t +xt/cli/freeze.t +xt/cli/help.t +xt/cli/install.t +xt/cli/json_pp.t +xt/cli/mirror.t +xt/cli/mismatch.t +xt/cli/no_cpanfile.t +xt/cli/perl.t +xt/cli/snapshot.t +xt/cli/subdir.t +xt/cli/tree.t +xt/cli/update.t +xt/cli/version.t +xt/cli/without.t +xt/mirror/authors/id/M/MI/MIYAGAWA/Hash-MultiValue-0.08.tar.gz +xt/mirror/modules/02packages.details.txt +xt/mirror/modules/02packages.details.txt.gz diff --git a/META.json b/META.json new file mode 100644 index 0000000..614fef4 --- /dev/null +++ b/META.json @@ -0,0 +1,96 @@ +{ + "abstract" : "Perl module dependency manager (aka Bundler for Perl)", + "author" : [ + "Tatsuhiko Miyagawa" + ], + "dynamic_config" : 0, + "generated_by" : "Dist::Zilla version 5.036, Dist::Milla version v1.0.15, CPAN::Meta::Converter version 2.150001", + "license" : [ + "perl_5" + ], + "meta-spec" : { + "url" : "http://search.cpan.org/perldoc?CPAN::Meta::Spec", + "version" : 2 + }, + "name" : "Carton", + "no_index" : { + "directory" : [ + "t", + "xt", + "inc", + "share", + "eg", + "examples" + ] + }, + "prereqs" : { + "configure" : { + "requires" : { + "ExtUtils::MakeMaker" : "0", + "version" : "0.77" + } + }, + "develop" : { + "requires" : { + "Capture::Tiny" : "0", + "Dist::Milla" : "v1.0.15", + "Test::More" : "0.9", + "Test::Pod" : "1.41", + "Test::Requires" : "0" + } + }, + "runtime" : { + "recommends" : { + "App::FatPacker" : "0.009018", + "File::pushd" : "0" + }, + "requires" : { + "App::cpanminus" : "1.703", + "CPAN::Meta" : "2.120921", + "CPAN::Meta::Requirements" : "2.121", + "Class::Tiny" : "1.001", + "Getopt::Long" : "2.39", + "JSON" : "2.53", + "Module::CPANfile" : "0.9031", + "Module::CoreList" : "0", + "Module::Metadata" : "1.000003", + "Module::Reader" : "0.002", + "Path::Tiny" : "0.033", + "Try::Tiny" : "0.09", + "parent" : "0.223", + "perl" : "v5.8.5" + } + } + }, + "release_status" : "stable", + "resources" : { + "bugtracker" : { + "web" : "https://github.com/perl-carton/carton/issues" + }, + "homepage" : "https://github.com/perl-carton/carton", + "repository" : { + "type" : "git", + "url" : "https://github.com/perl-carton/carton.git", + "web" : "https://github.com/perl-carton/carton" + } + }, + "version" : "v1.0.21", + "x_contributors" : [ + "Christian Walde <walde.christian@googlemail.com>", + "David Golden <dagolden@cpan.org>", + "David Steinbrunner <dsteinbrunner@pobox.com>", + "ikasam_a <masaki.nakagawa@gmail.com>", + "Jonathan Steinert <hachi@fastly.com>", + "Kan Fushihara <kan.fushihara@gmail.com>", + "Masahiro Chiba <chiba@everqueue.com>", + "NAKAGAWA Masaki <masaki.nakagawa@gmail.com>", + "Olaf Alders <olaf@wundersolutions.com>", + "Pedro Figueiredo <me@pedrofigueiredo.org>", + "Peter Oliver <git@mavit.org.uk>", + "shiba_yu36 <shibayu36@gmail.com>", + "Tatsuhiko Miyagawa <miyagawa@bulknews.net>", + "Tatsuhiko Miyagawa <miyagawa@gmail.com>", + "Yanick Champoux <yanick@babyl.dyndns.org>" + ] +} + diff --git a/META.yml b/META.yml new file mode 100644 index 0000000..9e01fae --- /dev/null +++ b/META.yml @@ -0,0 +1,62 @@ +--- +abstract: 'Perl module dependency manager (aka Bundler for Perl)' +author: + - 'Tatsuhiko Miyagawa' +build_requires: {} +configure_requires: + ExtUtils::MakeMaker: '0' + version: '0.77' +dynamic_config: 0 +generated_by: 'Dist::Zilla version 5.036, Dist::Milla version v1.0.15, CPAN::Meta::Converter version 2.150001' +license: perl +meta-spec: + url: http://module-build.sourceforge.net/META-spec-v1.4.html + version: '1.4' +name: Carton +no_index: + directory: + - t + - xt + - inc + - share + - eg + - examples +recommends: + App::FatPacker: '0.009018' + File::pushd: '0' +requires: + App::cpanminus: '1.703' + CPAN::Meta: '2.120921' + CPAN::Meta::Requirements: '2.121' + Class::Tiny: '1.001' + Getopt::Long: '2.39' + JSON: '2.53' + Module::CPANfile: '0.9031' + Module::CoreList: '0' + Module::Metadata: '1.000003' + Module::Reader: '0.002' + Path::Tiny: '0.033' + Try::Tiny: '0.09' + parent: '0.223' + perl: v5.8.5 +resources: + bugtracker: https://github.com/perl-carton/carton/issues + homepage: https://github.com/perl-carton/carton + repository: https://github.com/perl-carton/carton.git +version: v1.0.21 +x_contributors: + - 'Christian Walde <walde.christian@googlemail.com>' + - 'David Golden <dagolden@cpan.org>' + - 'David Steinbrunner <dsteinbrunner@pobox.com>' + - 'ikasam_a <masaki.nakagawa@gmail.com>' + - 'Jonathan Steinert <hachi@fastly.com>' + - 'Kan Fushihara <kan.fushihara@gmail.com>' + - 'Masahiro Chiba <chiba@everqueue.com>' + - 'NAKAGAWA Masaki <masaki.nakagawa@gmail.com>' + - 'Olaf Alders <olaf@wundersolutions.com>' + - 'Pedro Figueiredo <me@pedrofigueiredo.org>' + - 'Peter Oliver <git@mavit.org.uk>' + - 'shiba_yu36 <shibayu36@gmail.com>' + - 'Tatsuhiko Miyagawa <miyagawa@bulknews.net>' + - 'Tatsuhiko Miyagawa <miyagawa@gmail.com>' + - 'Yanick Champoux <yanick@babyl.dyndns.org>' diff --git a/Makefile.PL b/Makefile.PL new file mode 100644 index 0000000..fda5390 --- /dev/null +++ b/Makefile.PL @@ -0,0 +1,73 @@ +# This file was automatically generated by Dist::Zilla::Plugin::MakeMaker v5.036. +use strict; +use warnings; + +use 5.008005; + +use ExtUtils::MakeMaker; + +my %WriteMakefileArgs = ( + "ABSTRACT" => "Perl module dependency manager (aka Bundler for Perl)", + "AUTHOR" => "Tatsuhiko Miyagawa", + "CONFIGURE_REQUIRES" => { + "ExtUtils::MakeMaker" => 0, + "version" => "0.77" + }, + "DISTNAME" => "Carton", + "EXE_FILES" => [ + "script/carton" + ], + "LICENSE" => "perl", + "MIN_PERL_VERSION" => "5.008005", + "NAME" => "Carton", + "PREREQ_PM" => { + "App::cpanminus" => "1.703", + "CPAN::Meta" => "2.120921", + "CPAN::Meta::Requirements" => "2.121", + "Class::Tiny" => "1.001", + "Getopt::Long" => "2.39", + "JSON" => "2.53", + "Module::CPANfile" => "0.9031", + "Module::CoreList" => 0, + "Module::Metadata" => "1.000003", + "Module::Reader" => "0.002", + "Path::Tiny" => "0.033", + "Try::Tiny" => "0.09", + "parent" => "0.223" + }, + "VERSION" => "v1.0.21", + "test" => { + "TESTS" => "t/*.t" + } +); + + +my %FallbackPrereqs = ( + "App::cpanminus" => "1.703", + "CPAN::Meta" => "2.120921", + "CPAN::Meta::Requirements" => "2.121", + "Class::Tiny" => "1.001", + "ExtUtils::MakeMaker" => 0, + "Getopt::Long" => "2.39", + "JSON" => "2.53", + "Module::CPANfile" => "0.9031", + "Module::CoreList" => 0, + "Module::Metadata" => "1.000003", + "Module::Reader" => "0.002", + "Path::Tiny" => "0.033", + "Try::Tiny" => "0.09", + "parent" => "0.223", + "version" => "0.77" +); + + +unless ( eval { ExtUtils::MakeMaker->VERSION(6.63_03) } ) { + delete $WriteMakefileArgs{TEST_REQUIRES}; + delete $WriteMakefileArgs{BUILD_REQUIRES}; + $WriteMakefileArgs{PREREQ_PM} = \%FallbackPrereqs; +} + +delete $WriteMakefileArgs{CONFIGURE_REQUIRES} + unless eval { ExtUtils::MakeMaker->VERSION(6.52) }; + +WriteMakefile(%WriteMakefileArgs); @@ -0,0 +1,165 @@ +NAME + + Carton - Perl module dependency manager (aka Bundler for Perl) + +SYNOPSIS + + # On your development environment + > cat cpanfile + requires 'Plack', '0.9980'; + requires 'Starman', '0.2000'; + + > carton install + > git add cpanfile cpanfile.snapshot + > git commit -m "add Plack and Starman" + + # Other developer's machine, or on a deployment box + > carton install + > carton exec starman -p 8080 myapp.psgi + +AVAILABILITY + + Carton only works with perl installation with the complete set of core + modules. If you use perl installed by a vendor package with modules + stripped from core, Carton is not expected to work correctly. + + Also, Carton requires you to run your command/application with carton + exec command, which means it's difficult or impossible to run in an + embedded perl use case such as mod_perl. + +DESCRIPTION + + carton is a command line tool to track the Perl module dependencies for + your Perl application. Dependencies are declared using cpanfile format, + and the managed dependencies are tracked in a cpanfile.snapshot file, + which is meant to be version controlled, and the snapshot file allows + other developers of your application will have the exact same versions + of the modules. + + For cpanfile syntax, see cpanfile documentation. + +TUTORIAL + + Initializing the environment + + carton will use the local directory to install modules into. You're + recommended to exclude these directories from the version control + system. + + > echo local/ >> .gitignore + > git add cpanfile cpanfile.snapshot + > git commit -m "Start using carton" + + Tracking the dependencies + + You can manage the dependencies of your application via cpanfile. + + # cpanfile + requires 'Plack', '0.9980'; + requires 'Starman', '0.2000'; + + And then you can install these dependencies via: + + > carton install + + The modules are installed into your local directory, and the + dependencies tree and version information are analyzed and saved into + cpanfile.snapshot in your directory. + + Make sure you add cpanfile and cpanfile.snapshot to your version + controlled repository and commit changes as you update dependencies. + This will ensure that other developers on your app, as well as your + deployment environment, use exactly the same versions of the modules + you just installed. + + > git add cpanfile cpanfile.snapshot + > git commit -m "Added Plack and Starman" + + Deploying your application + + Once you've done installing all the dependencies, you can push your + application directory to a remote machine (excluding local and .carton) + and run the following command: + + > carton install --deployment + + This will look at the cpanfile.snapshot and install the exact same + versions of the dependencies into local, and now your application is + ready to run. + + The --deployment flag makes sure that carton will only install modules + and versions available in your snapshot, and won't fallback to query + for CPAN Meta DB for missing modules. + + Bundling modules + + carton can bundle all the tarballs for your dependencies into a + directory so that you can even install dependencies that are not + available on CPAN, such as internal distribution aka DarkPAN. + + > carton bundle + + will bundle these tarballs into vendor/cache directory, and + + > carton install --cached + + will install modules using this local cache. Combined with --deployment + option, you can avoid querying for a database like CPAN Meta DB or + downloading files from CPAN mirrors upon deployment time. + +PERL VERSIONS + + When you take a snapshot in one perl version and deploy on another + (different) version, you might have troubles with core modules. + + The simplest solution, which might not work for everybody, is to use + the same version of perl in the development and deployment. + + To enforce that, you're recommended to use plenv and .perl-version to + lock perl versions in development. + + You can also specify the minimum perl required in cpanfile: + + requires 'perl', '5.16.3'; + + and carton (and cpanm) will give you errors when deployed on hosts with + perl lower than the specified version. + +COMMUNITY + + https://github.com/miyagawa/carton + + Code repository, Wiki and Issue Tracker + + irc://irc.perl.org/#carton + + IRC chat room + +AUTHOR + + Tatsuhiko Miyagawa + +COPYRIGHT + + Tatsuhiko Miyagawa 2011- + +LICENSE + + This software is licensed under the same terms as Perl itself. + +SEE ALSO + + cpanm + + cpanfile + + Bundler <http://gembundler.com/> + + pip <http://pypi.python.org/pypi/pip> + + npm <http://npmjs.org/> + + perlrocks <https://github.com/gugod/perlrocks> + + only + diff --git a/cpanfile b/cpanfile new file mode 100644 index 0000000..bdaa6cd --- /dev/null +++ b/cpanfile @@ -0,0 +1,32 @@ +on configure => sub { + requires 'version', 0.77; +}; + +requires 'perl', '5.8.5'; + +requires 'JSON', 2.53; +requires 'Module::Metadata', 1.000003; +requires 'Module::CPANfile', 0.9031; + +requires 'Try::Tiny', 0.09; +requires 'parent', 0.223; +requires 'Getopt::Long', 2.39; +requires 'Class::Tiny', 1.001; +requires 'Path::Tiny', 0.033; + +requires 'App::cpanminus', 1.7030; + +requires 'CPAN::Meta', 2.120921; +requires 'CPAN::Meta::Requirements', 2.121; +requires 'Module::CoreList'; + +# for fatpack +requires 'Module::Reader', 0.002; +recommends 'File::pushd'; +recommends 'App::FatPacker', 0.009018; + +on develop => sub { + requires 'Test::More', 0.90; + requires 'Test::Requires'; + requires 'Capture::Tiny'; +}; diff --git a/dist.ini b/dist.ini new file mode 100644 index 0000000..5abd671 --- /dev/null +++ b/dist.ini @@ -0,0 +1,3 @@ +name = Carton +[@Milla] +installer = MakeMaker diff --git a/lib/Carton.pm b/lib/Carton.pm new file mode 100644 index 0000000..7546c77 --- /dev/null +++ b/lib/Carton.pm @@ -0,0 +1,179 @@ +package Carton; +use strict; +use 5.008_005; +use version; our $VERSION = version->declare("v1.0.21"); + +1; +__END__ + +=head1 NAME + +Carton - Perl module dependency manager (aka Bundler for Perl) + +=head1 SYNOPSIS + + # On your development environment + > cat cpanfile + requires 'Plack', '0.9980'; + requires 'Starman', '0.2000'; + + > carton install + > git add cpanfile cpanfile.snapshot + > git commit -m "add Plack and Starman" + + # Other developer's machine, or on a deployment box + > carton install + > carton exec starman -p 8080 myapp.psgi + +=head1 AVAILABILITY + +Carton only works with perl installation with the complete set of core +modules. If you use perl installed by a vendor package with modules +stripped from core, Carton is not expected to work correctly. + +Also, Carton requires you to run your command/application with +C<carton exec> command, which means it's difficult or impossible to +run in an embedded perl use case such as mod_perl. + +=head1 DESCRIPTION + +carton is a command line tool to track the Perl module dependencies +for your Perl application. Dependencies are declared using L<cpanfile> +format, and the managed dependencies are tracked in a +I<cpanfile.snapshot> file, which is meant to be version controlled, +and the snapshot file allows other developers of your application will +have the exact same versions of the modules. + +For C<cpanfile> syntax, see L<cpanfile> documentation. + +=head1 TUTORIAL + +=head2 Initializing the environment + +carton will use the I<local> directory to install modules into. You're +recommended to exclude these directories from the version control +system. + + > echo local/ >> .gitignore + > git add cpanfile cpanfile.snapshot + > git commit -m "Start using carton" + +=head2 Tracking the dependencies + +You can manage the dependencies of your application via C<cpanfile>. + + # cpanfile + requires 'Plack', '0.9980'; + requires 'Starman', '0.2000'; + +And then you can install these dependencies via: + + > carton install + +The modules are installed into your I<local> directory, and the +dependencies tree and version information are analyzed and saved into +I<cpanfile.snapshot> in your directory. + +Make sure you add I<cpanfile> and I<cpanfile.snapshot> to your version +controlled repository and commit changes as you update +dependencies. This will ensure that other developers on your app, as +well as your deployment environment, use exactly the same versions of +the modules you just installed. + + > git add cpanfile cpanfile.snapshot + > git commit -m "Added Plack and Starman" + +=head2 Deploying your application + +Once you've done installing all the dependencies, you can push your +application directory to a remote machine (excluding I<local> and +I<.carton>) and run the following command: + + > carton install --deployment + +This will look at the I<cpanfile.snapshot> and install the exact same +versions of the dependencies into I<local>, and now your application +is ready to run. + +The C<--deployment> flag makes sure that carton will only install +modules and versions available in your snapshot, and won't fallback to +query for CPAN Meta DB for missing modules. + +=head2 Bundling modules + +carton can bundle all the tarballs for your dependencies into a +directory so that you can even install dependencies that are not +available on CPAN, such as internal distribution aka DarkPAN. + + > carton bundle + +will bundle these tarballs into I<vendor/cache> directory, and + + > carton install --cached + +will install modules using this local cache. Combined with +C<--deployment> option, you can avoid querying for a database like +CPAN Meta DB or downloading files from CPAN mirrors upon deployment +time. + +=head1 PERL VERSIONS + +When you take a snapshot in one perl version and deploy on another +(different) version, you might have troubles with core modules. + +The simplest solution, which might not work for everybody, is to use +the same version of perl in the development and deployment. + +To enforce that, you're recommended to use L<plenv> and +C<.perl-version> to lock perl versions in development. + +You can also specify the minimum perl required in C<cpanfile>: + + requires 'perl', '5.16.3'; + +and carton (and cpanm) will give you errors when deployed on hosts +with perl lower than the specified version. + +=head1 COMMUNITY + +=over 4 + +=item L<https://github.com/miyagawa/carton> + +Code repository, Wiki and Issue Tracker + +=item L<irc://irc.perl.org/#carton> + +IRC chat room + +=back + +=head1 AUTHOR + +Tatsuhiko Miyagawa + +=head1 COPYRIGHT + +Tatsuhiko Miyagawa 2011- + +=head1 LICENSE + +This software is licensed under the same terms as Perl itself. + +=head1 SEE ALSO + +L<cpanm> + +L<cpanfile> + +L<Bundler|http://gembundler.com/> + +L<pip|http://pypi.python.org/pypi/pip> + +L<npm|http://npmjs.org/> + +L<perlrocks|https://github.com/gugod/perlrocks> + +L<only> + +=cut diff --git a/lib/Carton/Builder.pm b/lib/Carton/Builder.pm new file mode 100644 index 0000000..de456d7 --- /dev/null +++ b/lib/Carton/Builder.pm @@ -0,0 +1,114 @@ +package Carton::Builder; +use strict; +use Class::Tiny { + mirror => undef, + index => undef, + cascade => sub { 1 }, + without => sub { [] }, + cpanfile => undef, + fatscript => sub { $_[0]->_build_fatscript }, +}; + +sub effective_mirrors { + my $self = shift; + + # push default CPAN mirror always, as a fallback + # TODO don't pass fallback if --cached is set? + + my @mirrors = ($self->mirror); + push @mirrors, Carton::Mirror->default if $self->custom_mirror; + push @mirrors, Carton::Mirror->new('http://backpan.perl.org/'); + + @mirrors; +} + +sub custom_mirror { + my $self = shift; + ! $self->mirror->is_default; +} + +sub bundle { + my($self, $path, $cache_path, $snapshot) = @_; + + for my $dist ($snapshot->distributions) { + my $source = $path->child("cache/authors/id/" . $dist->pathname); + my $target = $cache_path->child("authors/id/" . $dist->pathname); + + if ($source->exists) { + warn "Copying ", $dist->pathname, "\n"; + $target->parent->mkpath; + $source->copy($target) or warn "$target: $!"; + } else { + warn "Couldn't find @{[ $dist->pathname ]}\n"; + } + } +} + +sub install { + my($self, $path) = @_; + + $self->run_cpanm( + "-L", $path, + (map { ("--mirror", $_->url) } $self->effective_mirrors), + ( $self->index ? ("--mirror-index", $self->index) : () ), + ( $self->cascade ? "--cascade-search" : () ), + ( $self->custom_mirror ? "--mirror-only" : () ), + "--save-dists", "$path/cache", + $self->groups, + "--cpanfile", $self->cpanfile, + "--installdeps", $self->cpanfile->dirname, + ) or die "Installing modules failed\n"; +} + +sub groups { + my $self = shift; + + # TODO support --without test (don't need test on deployment) + my @options = ('--with-all-features', '--with-develop'); + + for my $group (@{$self->without}) { + push @options, '--without-develop' if $group eq 'develop'; + push @options, "--without-feature=$group"; + } + + return @options; +} + +sub update { + my($self, $path, @modules) = @_; + + $self->run_cpanm( + "-L", $path, + (map { ("--mirror", $_->url) } $self->effective_mirrors), + ( $self->custom_mirror ? "--mirror-only" : () ), + "--save-dists", "$path/cache", + @modules + ) or die "Updating modules failed\n"; +} + +sub _build_fatscript { + my $self = shift; + + my $fatscript; + if ($Carton::Fatpacked) { + require Module::Reader; + my $content = Module::Reader::module_content('App::cpanminus::fatscript') + or die "Can't locate App::cpanminus::fatscript"; + $fatscript = Path::Tiny->tempfile; + $fatscript->spew($content); + } else { + require Module::Metadata; + $fatscript = Module::Metadata->find_module_by_name("App::cpanminus::fatscript") + or die "Can't locate App::cpanminus::fatscript"; + } + + return $fatscript; +} + +sub run_cpanm { + my($self, @args) = @_; + local $ENV{PERL_CPANM_OPT}; + !system $^X, $self->fatscript, "--quiet", "--notest", @args; +} + +1; diff --git a/lib/Carton/CLI.pm b/lib/Carton/CLI.pm new file mode 100644 index 0000000..b990ad2 --- /dev/null +++ b/lib/Carton/CLI.pm @@ -0,0 +1,396 @@ +package Carton::CLI; +use strict; +use warnings; +use Config; +use Getopt::Long; +use Path::Tiny; +use Try::Tiny; +use Module::CoreList; +use Scalar::Util qw(blessed); + +use Carton; +use Carton::Builder; +use Carton::Mirror; +use Carton::Snapshot; +use Carton::Util; +use Carton::Environment; +use Carton::Error; + +use constant { SUCCESS => 0, INFO => 1, WARN => 2, ERROR => 3 }; + +our $UseSystem = 0; # 1 for unit testing + +use Class::Tiny { + verbose => undef, + carton => sub { $_[0]->_build_carton }, + mirror => sub { $_[0]->_build_mirror }, +}; + +sub _build_mirror { + my $self = shift; + Carton::Mirror->new($ENV{PERL_CARTON_MIRROR} || $Carton::Mirror::DefaultMirror); +} + +sub run { + my($self, @args) = @_; + + my @commands; + my $p = Getopt::Long::Parser->new( + config => [ "no_ignore_case", "pass_through" ], + ); + $p->getoptionsfromarray( + \@args, + "h|help" => sub { unshift @commands, 'help' }, + "v|version" => sub { unshift @commands, 'version' }, + "verbose!" => sub { $self->verbose($_[1]) }, + ); + + push @commands, @args; + + my $cmd = shift @commands || 'install'; + + my $code = try { + my $call = $self->can("cmd_$cmd") + or Carton::Error::CommandNotFound->throw(error => "Could not find command '$cmd'"); + $self->$call(@commands); + return 0; + } catch { + die $_ unless blessed $_ && $_->can('rethrow'); + + if ($_->isa('Carton::Error::CommandExit')) { + return $_->code || 255; + } elsif ($_->isa('Carton::Error::CommandNotFound')) { + warn $_->error, "\n\n"; + $self->cmd_usage; + return 255; + } elsif ($_->isa('Carton::Error')) { + warn $_->error, "\n"; + return 255; + } + }; + + return $code; +} + +sub commands { + my $self = shift; + + no strict 'refs'; + map { s/^cmd_//; $_ } + grep { /^cmd_.*/ && $self->can($_) } sort keys %{__PACKAGE__."::"}; +} + +sub cmd_usage { + my $self = shift; + $self->print(<<HELP); +Usage: carton <command> + +where <command> is one of: + @{[ join ", ", $self->commands ]} + +Run carton -h <command> for help. +HELP +} + +sub parse_options { + my($self, $args, @spec) = @_; + my $p = Getopt::Long::Parser->new( + config => [ "no_auto_abbrev", "no_ignore_case" ], + ); + $p->getoptionsfromarray($args, @spec); +} + +sub parse_options_pass_through { + my($self, $args, @spec) = @_; + + my $p = Getopt::Long::Parser->new( + config => [ "no_auto_abbrev", "no_ignore_case", "pass_through" ], + ); + $p->getoptionsfromarray($args, @spec); + + # with pass_through keeps -- in args + shift @$args if $args->[0] && $args->[0] eq '--'; +} + +sub printf { + my $self = shift; + my $type = pop; + my($temp, @args) = @_; + $self->print(sprintf($temp, @args), $type); +} + +sub print { + my($self, $msg, $type) = @_; + my $fh = $type && $type >= WARN ? *STDERR : *STDOUT; + print {$fh} $msg; +} + +sub error { + my($self, $msg) = @_; + $self->print($msg, ERROR); + Carton::Error::CommandExit->throw; +} + +sub cmd_help { + my $self = shift; + my $module = $_[0] ? ("Carton::Doc::" . ucfirst $_[0]) : "Carton.pm"; + system "perldoc", $module; +} + +sub cmd_version { + my $self = shift; + $self->print("carton $Carton::VERSION\n"); +} + +sub cmd_bundle { + my($self, @args) = @_; + + my $env = Carton::Environment->build; + $env->snapshot->load; + + $self->print("Bundling modules using @{[$env->cpanfile]}\n"); + + my $builder = Carton::Builder->new( + mirror => $self->mirror, + cpanfile => $env->cpanfile, + ); + $builder->bundle($env->install_path, $env->vendor_cache, $env->snapshot); + + $self->printf("Complete! Modules were bundled into %s\n", $env->vendor_cache, SUCCESS); +} + +sub cmd_fatpack { + my($self, @args) = @_; + + my $env = Carton::Environment->build; + require Carton::Packer; + Carton::Packer->new->fatpack_carton($env->vendor_bin); +} + +sub cmd_install { + my($self, @args) = @_; + + my($install_path, $cpanfile_path, @without); + + $self->parse_options( + \@args, + "p|path=s" => \$install_path, + "cpanfile=s" => \$cpanfile_path, + "without=s" => sub { push @without, split /,/, $_[1] }, + "deployment!" => \my $deployment, + "cached!" => \my $cached, + ); + + my $env = Carton::Environment->build($cpanfile_path, $install_path); + $env->snapshot->load_if_exists; + + if ($deployment && !$env->snapshot->loaded) { + $self->error("--deployment requires cpanfile.snapshot: Run `carton install` and make sure cpanfile.snapshot is checked into your version control.\n"); + } + + my $builder = Carton::Builder->new( + cascade => 1, + mirror => $self->mirror, + without => \@without, + cpanfile => $env->cpanfile, + ); + + # TODO: --without with no .lock won't fetch the groups, resulting in insufficient requirements + + if ($deployment) { + $self->print("Installing modules using @{[$env->cpanfile]} (deployment mode)\n"); + $builder->cascade(0); + } else { + $self->print("Installing modules using @{[$env->cpanfile]}\n"); + } + + # TODO merge CPANfile git to mirror even if lock doesn't exist + if ($env->snapshot->loaded) { + my $index_file = $env->install_path->child("cache/modules/02packages.details.txt"); + $index_file->parent->mkpath; + + $env->snapshot->write_index($index_file); + $builder->index($index_file); + } + + if ($cached) { + $builder->mirror(Carton::Mirror->new($env->vendor_cache)); + } + + $builder->install($env->install_path); + + unless ($deployment) { + $env->cpanfile->load; + $env->snapshot->find_installs($env->install_path, $env->cpanfile->requirements); + $env->snapshot->save; + } + + $self->print("Complete! Modules were installed into @{[$env->install_path]}\n", SUCCESS); +} + +sub cmd_show { + my($self, @args) = @_; + + my $env = Carton::Environment->build; + $env->snapshot->load; + + for my $module (@args) { + my $dist = $env->snapshot->find($module) + or $self->error("Couldn't locate $module in cpanfile.snapshot\n"); + $self->print( $dist->name . "\n" ); + } +} + +sub cmd_list { + my($self, @args) = @_; + + my $format = 'name'; + + $self->parse_options( + \@args, + "distfile" => sub { $format = 'distfile' }, + ); + + my $env = Carton::Environment->build; + $env->snapshot->load; + + for my $dist ($env->snapshot->distributions) { + $self->print($dist->$format . "\n"); + } +} + +sub cmd_tree { + my($self, @args) = @_; + + my $env = Carton::Environment->build; + $env->snapshot->load; + $env->cpanfile->load; + + my %seen; + my $dumper = sub { + my($dependency, $reqs, $level) = @_; + return if $level == 0; + return Carton::Tree::STOP if $dependency->dist->is_core; + return Carton::Tree::STOP if $seen{$dependency->distname}++; + $self->printf( "%s%s (%s)\n", " " x ($level - 1), $dependency->module, $dependency->distname, INFO ); + }; + + $env->tree->walk_down($dumper); +} + +sub cmd_check { + my($self, @args) = @_; + + my $cpanfile_path; + $self->parse_options( + \@args, + "cpanfile=s" => \$cpanfile_path, + ); + + my $env = Carton::Environment->build($cpanfile_path); + $env->snapshot->load; + $env->cpanfile->load; + + # TODO remove snapshot + # TODO pass git spec to Requirements? + my $merged_reqs = $env->tree->merged_requirements; + + my @missing; + for my $module ($merged_reqs->required_modules) { + my $install = $env->snapshot->find_or_core($module); + if ($install) { + unless ($merged_reqs->accepts_module($module => $install->version_for($module))) { + push @missing, [ $module, 1, $install->version_for($module) ]; + } + } else { + push @missing, [ $module, 0 ]; + } + } + + if (@missing) { + $self->print("Following dependencies are not satisfied.\n", INFO); + for my $missing (@missing) { + my($module, $unsatisfied, $version) = @$missing; + if ($unsatisfied) { + $self->printf(" %s has version %s. Needs %s\n", + $module, $version, $merged_reqs->requirements_for_module($module), INFO); + } else { + $self->printf(" %s is not installed. Needs %s\n", + $module, $merged_reqs->requirements_for_module($module), INFO); + } + } + $self->printf("Run `carton install` to install them.\n", INFO); + Carton::Error::CommandExit->throw; + } else { + $self->print("cpanfile's dependencies are satisfied.\n", INFO); + } +} + +sub cmd_update { + my($self, @args) = @_; + + my $env = Carton::Environment->build; + $env->cpanfile->load; + + + my $cpanfile = Module::CPANfile->load($env->cpanfile); + @args = grep { $_ ne 'perl' } $env->cpanfile->required_modules unless @args; + + $env->snapshot->load; + + my @modules; + for my $module (@args) { + my $dist = $env->snapshot->find_or_core($module) + or $self->error("Could not find module $module.\n"); + next if $dist->is_core; + push @modules, "$module~" . $env->cpanfile->requirements_for_module($module); + } + + my $builder = Carton::Builder->new( + mirror => $self->mirror, + cpanfile => $env->cpanfile, + ); + $builder->update($env->install_path, @modules); + + $env->snapshot->find_installs($env->install_path, $env->cpanfile->requirements); + $env->snapshot->save; +} + +sub cmd_exec { + my($self, @args) = @_; + + my $env = Carton::Environment->build; + $env->snapshot->load; + + # allows -Ilib + @args = map { /^(-[I])(.+)/ ? ($1,$2) : $_ } @args; + + while (@args) { + if ($args[0] eq '-I') { + warn "exec -Ilib is deprecated. You might want to run: carton exec perl -Ilib ...\n"; + splice(@args, 0, 2); + } else { + last; + } + } + + $self->parse_options_pass_through(\@args); # to handle -- + + unless (@args) { + $self->error("carton exec needs a command to run.\n"); + } + + # PERL5LIB takes care of arch + my $path = $env->install_path; + local $ENV{PERL5LIB} = "$path/lib/perl5"; + local $ENV{PATH} = "$path/bin:$ENV{PATH}"; + + if ($UseSystem) { + system @args; + } else { + exec @args; + exit 127; # command not found + } +} + +1; diff --git a/lib/Carton/CPANfile.pm b/lib/Carton/CPANfile.pm new file mode 100644 index 0000000..a60d690 --- /dev/null +++ b/lib/Carton/CPANfile.pm @@ -0,0 +1,44 @@ +package Carton::CPANfile; +use Path::Tiny (); +use Module::CPANfile; + +use overload q{""} => sub { $_[0]->stringify }, fallback => 1; + +use subs 'path'; + +use Class::Tiny { + path => undef, + _cpanfile => undef, + requirements => sub { $_[0]->_build_requirements }, +}; + +sub stringify { shift->path->stringify(@_) } +sub dirname { shift->path->dirname(@_) } +sub prereqs { shift->_cpanfile->prereqs(@_) } +sub required_modules { shift->requirements->required_modules(@_) } +sub requirements_for_module { shift->requirements->requirements_for_module(@_) } + +sub path { + my $self = shift; + if (@_) { + $self->{path} = Path::Tiny->new($_[0]); + } else { + $self->{path}; + } +} + +sub load { + my $self = shift; + $self->_cpanfile( Module::CPANfile->load($self->path) ); +} + +sub _build_requirements { + my $self = shift; + my $reqs = CPAN::Meta::Requirements->new; + $reqs->add_requirements($self->prereqs->requirements_for($_, 'requires')) + for qw( configure build runtime test develop ); + $reqs->clear_requirement('perl'); + $reqs; +} + +1; diff --git a/lib/Carton/Dependency.pm b/lib/Carton/Dependency.pm new file mode 100644 index 0000000..d3e11ac --- /dev/null +++ b/lib/Carton/Dependency.pm @@ -0,0 +1,21 @@ +package Carton::Dependency; +use strict; +use Class::Tiny { + module => undef, + requirement => undef, + dist => undef, +}; + +sub requirements { shift->dist->requirements(@_) } + +sub distname { + my $self = shift; + $self->dist->name; +} + +sub version { + my $self = shift; + $self->dist->version_for($self->module); +} + +1; diff --git a/lib/Carton/Dist.pm b/lib/Carton/Dist.pm new file mode 100644 index 0000000..9310e70 --- /dev/null +++ b/lib/Carton/Dist.pm @@ -0,0 +1,37 @@ +package Carton::Dist; +use strict; +use Class::Tiny { + name => undef, + pathname => undef, + provides => sub { +{} }, + requirements => sub { $_[0]->_build_requirements }, +}; + +use CPAN::Meta; + +sub add_string_requirement { shift->requirements->add_string_requirement(@_) } +sub required_modules { shift->requirements->required_modules(@_) } +sub requirements_for_module { shift->requirements->requirements_for_module(@_) } + +sub is_core { 0 } + +sub distfile { + my $self = shift; + $self->pathname; +} + +sub _build_requirements { + CPAN::Meta::Requirements->new; +} + +sub provides_module { + my($self, $module) = @_; + exists $self->provides->{$module}; +} + +sub version_for { + my($self, $module) = @_; + $self->provides->{$module}{version}; +} + +1; diff --git a/lib/Carton/Dist/Core.pm b/lib/Carton/Dist/Core.pm new file mode 100644 index 0000000..760ce66 --- /dev/null +++ b/lib/Carton/Dist/Core.pm @@ -0,0 +1,23 @@ +package Carton::Dist::Core; +use strict; +use parent 'Carton::Dist'; + +use Class::Tiny qw( module_version ); + +sub BUILDARGS { + my($class, %args) = @_; + + # TODO represent dual-life + $args{name} =~ s/::/-/g; + + \%args; +} + +sub is_core { 1 } + +sub version_for { + my($self, $module) = @_; + $self->module_version; +} + +1; diff --git a/lib/Carton/Doc/Bundle.pod b/lib/Carton/Doc/Bundle.pod new file mode 100644 index 0000000..68ce8cc --- /dev/null +++ b/lib/Carton/Doc/Bundle.pod @@ -0,0 +1,20 @@ +=head1 NAME + +Carton::Doc::Bundle - Bundle cached tarballs in vendor/cache + +=head1 SYNOPSIS + + carton bundle + +=head1 DESCRIPTION + +This command bundles cached tarballs into C<vendor/cache> +directory. These tarballs have been cached in C<local/cache> while +resolving dependencies in the snapshot file.snapshot. + +Bundled modules can be committed to a version control system, or +transferred to another host with scp/rsync etc. to use with C<carton +install --cached>. + +See also C<carton fatpack> that generates C<carton> executable in +C<vendor/bin>. diff --git a/lib/Carton/Doc/Check.pod b/lib/Carton/Doc/Check.pod new file mode 100644 index 0000000..5283a7a --- /dev/null +++ b/lib/Carton/Doc/Check.pod @@ -0,0 +1,24 @@ +=head1 NAME + +Carton::Doc::Check - Check if your cpanfile and local environment are in sync + +=head1 SYNOPSIS + + carton check + +=head1 DESCRIPTION + +This command checks the consistency between your C<cpanfile>, +C<cpanfile.snapshot> and the local environment. + +=head2 MISSING MODULES + +If one or more of the modules specified in your I<cpanfile> are not +found in your snapshot, C<carton check> will warn you about this: + + $ carton check + Following dependencies are not satisfied. + JSON has version 2.51. Needs 2.52 + Run `carton install` to install them. + +You can run C<carton install> again to reinstall these missing dependencies. diff --git a/lib/Carton/Doc/Exec.pod b/lib/Carton/Doc/Exec.pod new file mode 100644 index 0000000..9eeca09 --- /dev/null +++ b/lib/Carton/Doc/Exec.pod @@ -0,0 +1,19 @@ +=head1 NAME + +Carton::Doc::Exec - execute your script in a carton local environment + +=head1 SYNOPSIS + + carton exec perl myscript.pl + +=head1 DESCRIPTION + +This command allows you to run your script in an isolated carton local +environment, which means the perl 5 library path C<@INC> are the only +ones from perl's core library path, carton's library path +(i.e. C<local/lib/perl5>) and the current directory. + +This is useful to make sure your scripts and application use the exact +same versions of the modules in your library path, and are not using +any of the modules you accidentally installed into your system perl or +perlbrew's site library path. diff --git a/lib/Carton/Doc/FAQ.pod b/lib/Carton/Doc/FAQ.pod new file mode 100644 index 0000000..ec9a575 --- /dev/null +++ b/lib/Carton/Doc/FAQ.pod @@ -0,0 +1,112 @@ +=head1 NAME + +Carton::Doc::FAQ - Frequently Asked Questions + +=head1 QUESTIONS + +=head2 It looks useful, but what is the use case of this tool? + +The particular problem that carton is trying to address is this: + +You develop a Perl-based application, possibly but not limited to +webapps, with dozens of CPAN module dependencies. You install these +modules on your development machine, and describe these dependencies +in your I<cpanfile>. + +Now you get a production environment, either on PaaS provider or some +VPS, you install the dependencies using C<cpanm --installdeps .> and +it will pull all the latest releases from CPAN as of today and +everything just works. + +A few weeks later, your application becomes more popular, and you +think you need another machine to serve more requests. You set up +another machine with vanilla perl installation and install the +dependencies the same way. That will pull the I<latest> releases from +CPAN I<on that date>, rather than the same as what you have today. + +And that is the problem. It's not likely that everything just breaks +one day, but there's always a chance that one of the dependencies +breaks an API compatibility, or just uploaded a buggy version to CPAN +on that particular day. + +Carton allows you to I<lock> these dependencies into a version +controlled system, so that every time you deploy from a checkout, it +is guaranteed that all the same versions are installed into the local +environment. + +=head2 How is this different from Pinto or CPAN::Mini::Inject? + +carton definitely shares the goal with these private CPAN repository +management tool. But the main difference is that rather than creating +an actual CPAN-like repository that works with any CPAN clients, +Carton provides a way to install specific versions of distributions +from CPAN, or any CPAN-like mirrors (as well as git repositories in +the future version of Carton). + +Existing tools are designed to work I<with> CPAN clients such as +L<CPAN> or L<CPANPLUS>, and have accomplished that by working around +the CPAN mirror structure. + +carton I<internally> does the same thing, but its user interface is +centered around the installer, by implementing a wrapper for +L<cpanm|App::cpanminus>, so you can use the same commands in the +development mode and deployment mode. + +Carton automatically maintains the L<cpanfile.snapshot> file, which is meant +to be version controlled, inside your application directory. You don't +need a separate database, a directory or a web server to maintain +tarballs outside your application. The I<cpanfile.snapshot> file can always +be generated with C<carton install> command, and C<carton install> on +another machine can use the version in the snapshot. + +=head2 I already use Pinto to create DarkPAN mirror. Can I use Carton with this? + +Yes, by specifying Pinto mirror as your Carton mirror, you can take a +snapshot of your dependencies including your private modules on Pinto, +or whatever DarkPAN mirror. + +=head2 I'm already using perlbrew and local::lib. Can I use carton with this? + +If you're using L<local::lib> already with L<perlbrew> perl, possibly +with the new C<perlbrew lib> command, that's great! There are multiple +benefits over using L<perlbrew> and L<local::lib> for development and +use L<Carton> for deployment. + +The best practice and workflow to get your perl environment as clean +as possible with lots of modules installed for quick development would +be this: + +=over + +=item * + +Install fresh perl using perlbrew. The version must be the same +against the version you'll run on the production environment. + +=item * + +Once the installation is done, use C<perlbrew lib> command to create a +new local lib environment (let's call it I<devel>) and always use the +library as a default environment. Install as many modules as you would +like into the I<devel> library path. + +This ensures to have a vanilla C<perl> library path as clean as +possible. + +=item * + +When you build a new project that you want to manage dependencies via +Carton, turn off the I<devel> local::lib and create a new one, like +I<myapp>. Install L<Carton> and all of its dependencies to the +I<myapp> local::lib path. Then run C<carton install> like you +normally do. + +Becuase I<devel> and I<myapp> are isolated, the modules you installed +into I<devel> doesn't affect the process when carton builds the +dependency tree for your new project at all. This could often be +critical when you have a conditional dependency in your tree, like +L<Any::Moose>. + +=back + + diff --git a/lib/Carton/Doc/Fatpack.pod b/lib/Carton/Doc/Fatpack.pod new file mode 100644 index 0000000..15282e0 --- /dev/null +++ b/lib/Carton/Doc/Fatpack.pod @@ -0,0 +1,15 @@ +=head1 NAME + +Carton::Doc::Fatpack - Fatpack carton executable into vendor/bin + +=head1 SYNOPSIS + + carton fatpack + +=head1 DESCRIPTION + +This command creates a fatpack executable of C<carton> in +C<vendor/bin> directory, so that it can be used to bootstrap +deployment process, combined with C<carton bundle> and C<carton +install --cached>. + diff --git a/lib/Carton/Doc/Install.pod b/lib/Carton/Doc/Install.pod new file mode 100644 index 0000000..df3b58f --- /dev/null +++ b/lib/Carton/Doc/Install.pod @@ -0,0 +1,95 @@ +=head1 NAME + +Carton::Doc::Install - Install the dependencies + +=head1 SYNOPSIS + + carton install [--deployment] [--cached] [--path=PATH] [--without develop] + +=head1 DESCRIPTION + +Install the dependencies for your application. This command has two +modes and the behavior is slightly different. + +=head2 DEVELOPMENT MODE + +=over 4 + +=item carton install + +If you run C<carton install> without any arguments and if I<cpanfile> +exists, carton will scan dependencies from I<cpanfile> and install +the modules. + +=back + +If you run C<carton install> for the first time +(i.e. I<cpanfile.snapshot> does not exist), carton will fetch all the +modules specified, resolve dependencies and install all required +modules from CPAN. + +If I<cpanfile.snapshot> file does exist, carton will still try to install +modules specified or updated in I<cpanfile>, but uses I<cpanfile.snapshot> +for the dependency resolution, and then cascades to CPAN. + +carton will analyze all the dependencies and their version +information, and it is saved into I<cpanfile.snapshot> file. It is important +to add I<cpanfile.snapshot> file into a version controlled repository and +commit the changes as you update your dependencies. + +=head2 DEPLOYMENT MODE + +If you specify the C<--deployment> command line option or the +I<cpanfile.snapshot> exists, carton will only use the dependencies +specified in the I<cpanfile.snapshot> instead of resolving +dependencies. + +=head1 OPTIONS + +=over 4 + +=item --deployment + +Force the deployment mode. See L</"DEPLOYMENT MODE"> above. + +=item --cached + +Locate distribution tarballs in C<vendor/cache> rather than fetching +them from CPAN mirrors. This requires you to run C<carton bundle> +prior to the deployment and commit or sync the content of C<vendor> +directory to the other host you run C<carton install> on. + +=item --cpanfile + +Specify the alternate path for cpanfile. By default, C<carton install> +will look for the file C<cpanfile> in the current directory, then +upwards till the root directory, in case the command runs from a sub +directory. + +Carton assumes the directory where your cpanfile (or altenate path) +exists as a project root directory, and will look for the snapshot file as +well as install directory (C<local>) and C<vendor/cache> relative to it. + +=item --path + +Specify the path to install modules to. Defaults to I<local> in the +directory relative to where C<cpanfile> is. + +B<NOTE>: this option, as of version 1.0, is not preserved across +multiple runs of C<carton install> or other commands such as C<carton +list> or C<carton exec>. You can choose to set the path in +C<PERL_CARTON_PATH> environment variable to persist it across +commands. + +=item --without + +By default, C<carton install> will install all the phases for +dependencies, including C<develop>. You can specify phases or features +to exclude, in the comma separated list. + + carton install --deployment --without develop + +B<NOTE>: C<--without> for the initial installation (without +cpanfile.snapshot) is not supported at this moment. + +=back diff --git a/lib/Carton/Doc/List.pod b/lib/Carton/Doc/List.pod new file mode 100644 index 0000000..40c54e0 --- /dev/null +++ b/lib/Carton/Doc/List.pod @@ -0,0 +1,23 @@ +=head1 NAME + +Carton::Doc::List - List dependencies tracked in the cpanfile.snapshot file + +=head1 SYNOPSIS + + carton list + +=head1 DESCRIPTION + +List the dependencies and version information tracked in the +I<cpanfile.snapshot> file. This command by default displays the name of the +distribution (e.g. I<Foo-Bar-0.01>) in a flat list. + +=head1 OPTIONS + +=over 4 + +=item --distfile + +Displays the list of distributions in a distfile format (i.e. C<AUTHOR/Dist-1.23.tar.gz>) + +=back diff --git a/lib/Carton/Doc/Show.pod b/lib/Carton/Doc/Show.pod new file mode 100644 index 0000000..20618b4 --- /dev/null +++ b/lib/Carton/Doc/Show.pod @@ -0,0 +1,12 @@ +=head1 NAME + +Carton::Doc::Show - Show the module information + +=head1 SYNOPSIS + + carton show Module + +=head1 DESCRIPTION + +Displays the information about modules, distribution and its versions. + diff --git a/lib/Carton/Doc/Tree.pod b/lib/Carton/Doc/Tree.pod new file mode 100644 index 0000000..0080eef --- /dev/null +++ b/lib/Carton/Doc/Tree.pod @@ -0,0 +1,13 @@ +=head1 NAME + +Carton::Doc::Tree - Show the tree of dependency graph + +=head1 SYNOPSIS + + carton tree + +=head1 DESCRIPTION + +Displays the tree representation of dependency graph for your application. + + diff --git a/lib/Carton/Doc/Update.pod b/lib/Carton/Doc/Update.pod new file mode 100644 index 0000000..146f03e --- /dev/null +++ b/lib/Carton/Doc/Update.pod @@ -0,0 +1,40 @@ +=head1 NAME + +Carton::Doc::Update - Update the dependencies + +=head1 SYNOPSIS + + carton update [module] + +=head1 DESCRIPTION + +Update the dependencies version for your application. + +Carton is designed to update your dependency in a conservative way, +meaning that it doesn't update modules that aren't explicitly required +to. + +C<carton update> is a command to explicitly update one or all of +modules in your cpanfile to the latest available that satisfies the +requirements in cpanfile. + +=head1 EXAMPLE + +Suppose you have a cpanfile with: + + requires 'DBI', '1.600'; + requires 'Plack', '== 1.0011'; + +and then run C<carton install> to get DBI 1.610 (the latest at that +time) and Plack 1.0011 (as specified in the requirement). + +A few weeks later, DBI and Plack have been updated a couple of +times. Running C<carton install> I<won't> update the versions, because +the installed versions satisfy the requirements in C<cpanfile>. + +Running C<carton update> will update DBI to the latest version, say +1.611, because the version still satisfies the requirement. However, +it won't update Plack's version, since whatever latest version on CPAN +will not satisfy the Plack's requirement C<== 1.0011> because it wants +an exact version. + diff --git a/lib/Carton/Doc/Upgrading.pod b/lib/Carton/Doc/Upgrading.pod new file mode 100644 index 0000000..d7ad57e --- /dev/null +++ b/lib/Carton/Doc/Upgrading.pod @@ -0,0 +1,48 @@ +=head1 NAME + +Carton::Doc::Upgrading - Upgrading document + +=head1 UPGRADING + +Carton adds, changes and deprecates some features between major +releases in backward incompatible ways. Here's the list of major +changes between versions. See C<Changes> file for more details. + +=head2 v0.9 to v1.0 + +=over 4 + +=item * + +C<carton exec -Ilib> is deprecated. You must pass the optional include +path to perl interpreter in the normal way, like: + + carton exec perl -Ilib myscript + +Or make your script to take its own C<-I> option, like many command line +launcher does (i.e. plackup, prove) + + carton exec plackup -Ilib myapp.psgi + +=item * + +C<carton.lock> is now C<cpanfile.snapshot>. Its name and file format +has been changed. There's no automatic migration, but you can do: + + # run with Carton v0.9.64 + > carton install + + # upgrade to Carton v1.0 + > cpanm Carton + > carton install + > git add cpanfile.snapshot + > git rm carton.lock + +This process will most likely preserve modules in your local library. + +=item * + +cpanfile is now a requirement, and extracting requirements from build +files (C<Makefile.PL>, C<Build.PL>) is not supported. + +=back diff --git a/lib/Carton/Doc/Version.pod b/lib/Carton/Doc/Version.pod new file mode 100644 index 0000000..8ff36ad --- /dev/null +++ b/lib/Carton/Doc/Version.pod @@ -0,0 +1,11 @@ +=head1 NAME + +Carton::Doc::Version - Display version + +=head1 SYNOPSIS + + carton version + +=head1 DESCRIPTION + +This command displays the current version number of carton. diff --git a/lib/Carton/Environment.pm b/lib/Carton/Environment.pm new file mode 100644 index 0000000..6a58944 --- /dev/null +++ b/lib/Carton/Environment.pm @@ -0,0 +1,100 @@ +package Carton::Environment; +use strict; +use Carton::CPANfile; +use Carton::Snapshot; +use Carton::Error; +use Carton::Tree; +use Path::Tiny; + +use Class::Tiny { + cpanfile => undef, + snapshot => sub { $_[0]->_build_snapshot }, + install_path => sub { $_[0]->_build_install_path }, + vendor_cache => sub { $_[0]->_build_vendor_cache }, + tree => sub { $_[0]->_build_tree }, +}; + +sub _build_snapshot { + my $self = shift; + Carton::Snapshot->new(path => $self->cpanfile . ".snapshot"); +} + +sub _build_install_path { + my $self = shift; + if ($ENV{PERL_CARTON_PATH}) { + return Path::Tiny->new($ENV{PERL_CARTON_PATH}); + } else { + return $self->cpanfile->path->parent->child("local"); + } +} + +sub _build_vendor_cache { + my $self = shift; + Path::Tiny->new($self->install_path->dirname . "/vendor/cache"); +} + +sub _build_tree { + my $self = shift; + Carton::Tree->new(cpanfile => $self->cpanfile, snapshot => $self->snapshot); +} + +sub vendor_bin { + my $self = shift; + $self->vendor_cache->parent->child('bin'); +} + +sub build_with { + my($class, $cpanfile) = @_; + + $cpanfile = Path::Tiny->new($cpanfile)->absolute; + if ($cpanfile->is_file) { + return $class->new(cpanfile => Carton::CPANfile->new(path => $cpanfile)); + } else { + Carton::Error::CPANfileNotFound->throw(error => "Can't locate cpanfile: $cpanfile"); + } +} + +sub build { + my($class, $cpanfile_path, $install_path) = @_; + + my $self = $class->new; + + $cpanfile_path &&= Path::Tiny->new($cpanfile_path)->absolute; + + my $cpanfile = $self->locate_cpanfile($cpanfile_path || $ENV{PERL_CARTON_CPANFILE}); + if ($cpanfile && $cpanfile->is_file) { + $self->cpanfile( Carton::CPANfile->new(path => $cpanfile) ); + } else { + Carton::Error::CPANfileNotFound->throw(error => "Can't locate cpanfile: (@{[ $cpanfile_path || 'cpanfile' ]})"); + } + + $self->install_path( Path::Tiny->new($install_path)->absolute ) if $install_path; + + $self; +} + +sub locate_cpanfile { + my($self, $path) = @_; + + if ($path) { + return Path::Tiny->new($path)->absolute; + } + + my $current = Path::Tiny->cwd; + my $previous = ''; + + until ($current eq '/' or $current eq $previous) { + # TODO support PERL_CARTON_CPANFILE + my $try = $current->child('cpanfile'); + if ($try->is_file) { + return $try->absolute; + } + + ($previous, $current) = ($current, $current->parent); + } + + return; +} + +1; + diff --git a/lib/Carton/Error.pm b/lib/Carton/Error.pm new file mode 100644 index 0000000..b469eac --- /dev/null +++ b/lib/Carton/Error.pm @@ -0,0 +1,42 @@ +package Carton::Error; +use strict; +use overload '""' => sub { $_[0]->error }; +use Carp; + +sub throw { + my($class, @args) = @_; + die $class->new(@args); +} + +sub rethrow { + die $_[0]; +} + +sub new { + my($class, %args) = @_; + bless \%args, $class; +} + +sub error { + $_[0]->{error} || ref $_[0]; +} + +package Carton::Error::CommandNotFound; +use parent 'Carton::Error'; + +package Carton::Error::CommandExit; +use parent 'Carton::Error'; +sub code { $_[0]->{code} } + +package Carton::Error::CPANfileNotFound; +use parent 'Carton::Error'; + +package Carton::Error::SnapshotParseError; +use parent 'Carton::Error'; +sub path { $_[0]->{path} } + +package Carton::Error::SnapshotNotFound; +use parent 'Carton::Error'; +sub path { $_[0]->{path} } + +1; diff --git a/lib/Carton/Index.pm b/lib/Carton/Index.pm new file mode 100644 index 0000000..3ce215c --- /dev/null +++ b/lib/Carton/Index.pm @@ -0,0 +1,68 @@ +package Carton::Index; +use strict; +use Class::Tiny { + _packages => sub { +{} }, + generator => sub { require Carton; "Carton $Carton::VERSION" }, +}; + +sub add_package { + my($self, $package) = @_; + $self->_packages->{$package->name} = $package; # XXX ||= +} + +sub count { + my $self = shift; + scalar keys %{$self->_packages}; +} + +sub packages { + my $self = shift; + sort { lc $a->name cmp lc $b->name } values %{$self->_packages}; +} + +sub write { + my($self, $fh) = @_; + + print $fh <<EOF; +File: 02packages.details.txt +URL: http://www.perl.com/CPAN/modules/02packages.details.txt +Description: Package names found in cpanfile.snapshot +Columns: package name, version, path +Intended-For: Automated fetch routines, namespace documentation. +Written-By: @{[ $self->generator ]} +Line-Count: @{[ $self->count ]} +Last-Updated: @{[ scalar localtime ]} + +EOF + for my $p ($self->packages) { + print $fh $self->_format_line($p->name, $p->version || 'undef', $p->pathname); + } +} + +sub _format_line { + my($self, @row) = @_; + + # from PAUSE::mldistwatch::rewrite02 + my $one = 30; + my $two = 8; + + if (length $row[0] > $one) { + $one += 8 - length $row[1]; + $two = length $row[1]; + } + + sprintf "%-${one}s %${two}s %s\n", @row; +} + +sub pad { + my($str, $len, $left) = @_; + + my $howmany = $len - length($str); + return $str if $howmany <= 0; + + my $pad = " " x $howmany; + return $left ? "$pad$str" : "$str$pad"; +} + + +1; diff --git a/lib/Carton/Mirror.pm b/lib/Carton/Mirror.pm new file mode 100644 index 0000000..60bc937 --- /dev/null +++ b/lib/Carton/Mirror.pm @@ -0,0 +1,23 @@ +package Carton::Mirror; +use strict; +use Class::Tiny qw( url ); + +our $DefaultMirror = 'http://cpan.metacpan.org/'; + +sub BUILDARGS { + my($class, $url) = @_; + return { url => $url }; +} + +sub default { + my $class = shift; + $class->new($DefaultMirror); +} + +sub is_default { + my $self = shift; + $self->url eq $DefaultMirror; +} + +1; + diff --git a/lib/Carton/Package.pm b/lib/Carton/Package.pm new file mode 100644 index 0000000..6b1f381 --- /dev/null +++ b/lib/Carton/Package.pm @@ -0,0 +1,12 @@ +package Carton::Package; +use strict; +use Class::Tiny qw( name version pathname ); + +sub BUILDARGS { + my($class, @args) = @_; + return { name => $args[0], version => $args[1], pathname => $args[2] }; +} + +1; + + diff --git a/lib/Carton/Packer.pm b/lib/Carton/Packer.pm new file mode 100644 index 0000000..dc1a2cf --- /dev/null +++ b/lib/Carton/Packer.pm @@ -0,0 +1,97 @@ +package Carton::Packer; +use Class::Tiny; +use warnings NONFATAL => 'all'; +use App::FatPacker; +use File::pushd (); +use Path::Tiny (); +use CPAN::Meta (); +use File::Find (); + +sub fatpack_carton { + my($self, $dir) = @_; + + my $temp = Path::Tiny->tempdir; + my $pushd = File::pushd::pushd $temp; + + my $file = $temp->child('carton.pre.pl'); + + $file->spew(<<'EOF'); +#!/usr/bin/env perl +use strict; +use 5.008001; +use Carton::CLI; +$Carton::Fatpacked = 1; +exit Carton::CLI->new->run(@ARGV); +EOF + + my $fatpacked = $self->do_fatpack($file); + + my $executable = $dir->child('carton'); + warn "Bundling $executable\n"; + + $dir->mkpath; + $executable->spew($fatpacked); + chmod 0755, $executable; +} + +sub do_fatpack { + my($self, $file) = @_; + + my $packer = App::FatPacker->new; + + my @modules = split /\r?\n/, $packer->trace(args => [$file], use => $self->required_modules); + my @packlists = $packer->packlists_containing(\@modules); + $packer->packlists_to_tree(Path::Tiny->new('fatlib')->absolute, \@packlists); + + my $fatpacked = do { + local $SIG{__WARN__} = sub {}; + $packer->fatpack_file($file); + }; + + # HACK: File::Spec bundled into arch in < 5.16, but is loadable as pure-perl + use Config; + $fatpacked =~ s/\$fatpacked{"$Config{archname}\/(Cwd|File)/\$fatpacked{"$1/g; + + $fatpacked; +} + +sub required_modules { + my($self, $packer) = @_; + + my $meta = $self->installed_meta('Carton') + or die "Couldn't find install metadata for Carton"; + + my %excludes = ( + perl => 1, + 'ExtUtils::MakeMaker' => 1, + 'Module::Build' => 1, + ); + + my @requirements = grep !$excludes{$_}, + $meta->effective_prereqs->requirements_for('runtime', 'requires')->required_modules; + + return \@requirements; +} + +sub installed_meta { + my($self, $dist) = @_; + + my @meta; + my $finder = sub { + if (m!\b$dist-.*[\\/]MYMETA.json!) { + my $meta = CPAN::Meta->load_file($_); + push @meta, $meta if $meta->name eq $dist; + } + }; + + my @meta_dirs = grep -d, map "$_/.meta", @INC; + File::Find::find({ wanted => $finder, no_chdir => 1 }, @meta_dirs) + if @meta_dirs; + + # return the latest version + @meta = sort { version->new($b->version) cmp version->new($a->version) } @meta; + + return $meta[0]; +} + +1; diff --git a/lib/Carton/Snapshot.pm b/lib/Carton/Snapshot.pm new file mode 100644 index 0000000..9e57f18 --- /dev/null +++ b/lib/Carton/Snapshot.pm @@ -0,0 +1,191 @@ +package Carton::Snapshot; +use strict; +use Config; +use Carton::Dist; +use Carton::Dist::Core; +use Carton::Error; +use Carton::Package; +use Carton::Index; +use Carton::Util; +use Carton::Snapshot::Emitter; +use Carton::Snapshot::Parser; +use CPAN::Meta; +use CPAN::Meta::Requirements; +use File::Find (); +use Try::Tiny; +use Path::Tiny (); +use Module::CoreList; + +use constant CARTON_SNAPSHOT_VERSION => '1.0'; + +use subs 'path'; +use Class::Tiny { + path => undef, + version => sub { CARTON_SNAPSHOT_VERSION }, + loaded => undef, + _distributions => sub { +[] }, +}; + +sub BUILD { + my $self = shift; + $self->path( $self->{path} ); +} + +sub path { + my $self = shift; + if (@_) { + $self->{path} = Path::Tiny->new($_[0]); + } else { + $self->{path}; + } +} + +sub load_if_exists { + my $self = shift; + $self->load if $self->path->is_file; +} + +sub load { + my $self = shift; + + return 1 if $self->loaded; + + if ($self->path->is_file) { + my $parser = Carton::Snapshot::Parser->new; + $parser->parse($self->path->slurp_utf8, $self); + $self->loaded(1); + + return 1; + } else { + Carton::Error::SnapshotNotFound->throw( + error => "Can't find cpanfile.snapshot: Run `carton install` to build the snapshot file.", + path => $self->path, + ); + } +} + +sub save { + my $self = shift; + $self->path->spew_utf8( Carton::Snapshot::Emitter->new->emit($self) ); +} + +sub find { + my($self, $module) = @_; + (grep $_->provides_module($module), $self->distributions)[0]; +} + +sub find_or_core { + my($self, $module) = @_; + $self->find($module) || $self->find_in_core($module); +} + +sub find_in_core { + my($self, $module) = @_; + + if (exists $Module::CoreList::version{$]}{$module}) { + my $version = $Module::CoreList::version{$]}{$module}; # maybe undef + return Carton::Dist::Core->new(name => $module, module_version => $version); + } + + return; +} + +sub index { + my $self = shift; + + my $index = Carton::Index->new; + for my $package ($self->packages) { + $index->add_package($package); + } + + return $index; +} + +sub distributions { + @{$_[0]->_distributions}; +} + +sub add_distribution { + my($self, $dist) = @_; + push @{$self->_distributions}, $dist; +} + +sub packages { + my $self = shift; + + my @packages; + for my $dist ($self->distributions) { + while (my($package, $provides) = each %{$dist->provides}) { + # TODO what if duplicates? + push @packages, Carton::Package->new($package, $provides->{version}, $dist->pathname); + } + } + + return @packages; +} + +sub write_index { + my($self, $file) = @_; + + open my $fh, ">", $file or die $!; + $self->index->write($fh); +} + +sub find_installs { + my($self, $path, $reqs) = @_; + + my $libdir = "$path/lib/perl5/$Config{archname}/.meta"; + return {} unless -e $libdir; + + my @installs; + my $wanted = sub { + if ($_ eq 'install.json') { + push @installs, [ $File::Find::name, "$File::Find::dir/MYMETA.json" ]; + } + }; + File::Find::find($wanted, $libdir); + + my %installs; + + my $accepts = sub { + my $module = shift; + + return 0 unless $reqs->accepts_module($module->{name}, $module->{provides}{$module->{name}}{version}); + + if (my $exist = $installs{$module->{name}}) { + my $old_ver = version::->new($exist->{provides}{$module->{name}}{version}); + my $new_ver = version::->new($module->{provides}{$module->{name}}{version}); + return $new_ver >= $old_ver; + } else { + return 1; + } + }; + + for my $file (@installs) { + my $module = Carton::Util::load_json($file->[0]); + my $prereqs = -f $file->[1] ? CPAN::Meta->load_file($file->[1])->effective_prereqs : CPAN::Meta::Prereqs->new; + + my $reqs = CPAN::Meta::Requirements->new; + $reqs->add_requirements($prereqs->requirements_for($_, 'requires')) + for qw( configure build runtime ); + + if ($accepts->($module)) { + $installs{$module->{name}} = Carton::Dist->new( + name => $module->{dist}, + pathname => $module->{pathname}, + provides => $module->{provides}, + version => $module->{version}, + requirements => $reqs, + ); + } + } + + my @new_dists; + for my $module (sort keys %installs) { + push @new_dists, $installs{$module}; + } + + $self->_distributions(\@new_dists); +} + +1; diff --git a/lib/Carton/Snapshot/Emitter.pm b/lib/Carton/Snapshot/Emitter.pm new file mode 100644 index 0000000..9486ba7 --- /dev/null +++ b/lib/Carton/Snapshot/Emitter.pm @@ -0,0 +1,30 @@ +package Carton::Snapshot::Emitter; +use Class::Tiny; +use warnings NONFATAL => 'all'; + +sub emit { + my($self, $snapshot) = @_; + + my $data = ''; + $data .= "# carton snapshot format: version @{[$snapshot->version]}\n"; + $data .= "DISTRIBUTIONS\n"; + + for my $dist (sort { $a->name cmp $b->name } $snapshot->distributions) { + $data .= " @{[$dist->name]}\n"; + $data .= " pathname: @{[$dist->pathname]}\n"; + + $data .= " provides:\n"; + for my $package (sort keys %{$dist->provides}) { + $data .= " $package @{[$dist->provides->{$package}{version} || 'undef' ]}\n"; + } + + $data .= " requirements:\n"; + for my $module (sort $dist->required_modules) { + $data .= " $module @{[ $dist->requirements_for_module($module) || '0' ]}\n"; + } + } + + $data; +} + +1; diff --git a/lib/Carton/Snapshot/Parser.pm b/lib/Carton/Snapshot/Parser.pm new file mode 100644 index 0000000..21aa0c1 --- /dev/null +++ b/lib/Carton/Snapshot/Parser.pm @@ -0,0 +1,126 @@ +package Carton::Snapshot::Parser; +use Class::Tiny; +use warnings NONFATAL => 'all'; +use Carton::Dist; +use Carton::Error; + +my $machine = { + init => [ + { + re => qr/^\# carton snapshot format: version (1\.0)/, + code => sub { + my($stash, $snapshot, $ver) = @_; + $snapshot->version($ver); + }, + goto => 'section', + }, + # TODO support pasing error and version mismatch etc. + ], + section => [ + { + re => qr/^DISTRIBUTIONS$/, + goto => 'dists', + }, + { + re => qr/^__EOF__$/, + done => 1, + }, + ], + dists => [ + { + re => qr/^ (\S+)$/, + code => sub { $_[0]->{dist} = Carton::Dist->new(name => $1) }, + goto => 'distmeta', + }, + { + re => qr/^\S/, + goto => 'section', + redo => 1, + }, + ], + distmeta => [ + { + re => qr/^ pathname: (.*)$/, + code => sub { $_[0]->{dist}->pathname($1) }, + }, + { + re => qr/^\s{4}provides:$/, + code => sub { $_[0]->{property} = 'provides' }, + goto => 'properties', + }, + { + re => qr/^\s{4}requirements:$/, + code => sub { + $_[0]->{property} = 'requirements'; + }, + goto => 'properties', + }, + { + re => qr/^\s{0,2}\S/, + code => sub { + my($stash, $snapshot) = @_; + $snapshot->add_distribution($stash->{dist}); + %$stash = (); # clear + }, + goto => 'dists', + redo => 1, + }, + ], + properties => [ + { + re => qr/^\s{6}([0-9A-Za-z_:]+) ([v0-9\._,=\!<>\s]+|undef)/, + code => sub { + my($stash, $snapshot, $module, $version) = @_; + + if ($stash->{property} eq 'provides') { + $stash->{dist}->provides->{$module} = { version => $version }; + } else { + $stash->{dist}->add_string_requirement($module, $version); + } + }, + }, + { + re => qr/^\s{0,4}\S/, + goto => 'distmeta', + redo => 1, + }, + ], +}; + +sub parse { + my($self, $data, $snapshot) = @_; + + my @lines = split /\r?\n/, $data; + + my $state = $machine->{init}; + my $stash = {}; + + LINE: + for my $line (@lines, '__EOF__') { + last LINE unless @$state; + + STATE: { + for my $trans (@{$state}) { + if (my @match = $line =~ $trans->{re}) { + if (my $code = $trans->{code}) { + $code->($stash, $snapshot, @match); + } + if (my $goto = $trans->{goto}) { + $state = $machine->{$goto}; + if ($trans->{redo}) { + redo STATE; + } else { + next LINE; + } + } + + last STATE; + } + } + + Carton::Error::SnapshotParseError->throw(error => "Could not parse snapshot file."); + } + } +} + +1; diff --git a/lib/Carton/Tree.pm b/lib/Carton/Tree.pm new file mode 100644 index 0000000..6ce22a1 --- /dev/null +++ b/lib/Carton/Tree.pm @@ -0,0 +1,69 @@ +package Carton::Tree; +use strict; +use Carton::Dependency; + +use Class::Tiny qw( cpanfile snapshot ); + +use constant STOP => -1; + +sub walk_down { + my($self, $cb) = @_; + + my $dumper; $dumper = sub { + my($dependency, $reqs, $level, $parent) = @_; + + my $ret = $cb->($dependency, $reqs, $level); + return if $ret && $ret == STOP; + + local $parent->{$dependency->distname} = 1 if $dependency; + + for my $module (sort $reqs->required_modules) { + my $dependency = $self->dependency_for($module, $reqs); + if ($dependency->dist) { + next if $parent->{$dependency->distname}; + $dumper->($dependency, $dependency->requirements, $level + 1, $parent); + } else { + # no dist found in lock + } + } + }; + + $dumper->(undef, $self->cpanfile->requirements, 0, {}); + undef $dumper; +} + +sub dependency_for { + my($self, $module, $reqs) = @_; + + my $requirement = $reqs->requirements_for_module($module); + + my $dep = Carton::Dependency->new; + $dep->module($module); + $dep->requirement($requirement); + + if (my $dist = $self->snapshot->find_or_core($module)) { + $dep->dist($dist); + } + + return $dep; +} + +sub merged_requirements { + my $self = shift; + + my $merged_reqs = CPAN::Meta::Requirements->new; + + my %seen; + $self->walk_down(sub { + my($dependency, $reqs, $level) = @_; + return Carton::Tree::STOP if $dependency && $seen{$dependency->distname}++; + $merged_reqs->add_requirements($reqs); + }); + + $merged_reqs->clear_requirement('perl'); + $merged_reqs->finalize; + + $merged_reqs; +} + +1; diff --git a/lib/Carton/Util.pm b/lib/Carton/Util.pm new file mode 100644 index 0000000..cc9c775 --- /dev/null +++ b/lib/Carton/Util.pm @@ -0,0 +1,31 @@ +package Carton::Util; +use strict; +use warnings; + +sub load_json { + my $file = shift; + + open my $fh, "<", $file or die "$file: $!"; + from_json(join '', <$fh>); +} + +sub dump_json { + my($data, $file) = @_; + + open my $fh, ">", $file or die "$file: $!"; + binmode $fh; + print $fh to_json($data); +} + +sub from_json { + require JSON; + JSON::decode_json(@_); +} + +sub to_json { + my($data) = @_; + require JSON; + JSON->new->utf8->pretty->canonical->encode($data); +} + +1; diff --git a/script/carton b/script/carton new file mode 100755 index 0000000..86b9ad3 --- /dev/null +++ b/script/carton @@ -0,0 +1,27 @@ +#!perl +use strict; +use 5.008001; +use Carton::CLI; + +exit Carton::CLI->new->run(@ARGV); + +__END__ + +=head1 NAME + +carton - Perl module dependency manager + +=head1 SYNOPSIS + + > carton install + > carton exec ./myscript + +=head1 DESCRIPTION + +For more documentation, refer to L<Carton> by running C<perldoc Carton> or C<carton --help>. + +=head1 SEE ALSO + +L<Carton> + +=cut diff --git a/t/release-pod-syntax.t b/t/release-pod-syntax.t new file mode 100644 index 0000000..cdd6a6c --- /dev/null +++ b/t/release-pod-syntax.t @@ -0,0 +1,14 @@ +#!perl + +BEGIN { + unless ($ENV{RELEASE_TESTING}) { + require Test::More; + Test::More::plan(skip_all => 'these tests are for release candidate testing'); + } +} + +# This file was automatically generated by Dist::Zilla::Plugin::PodSyntaxTests. +use Test::More; +use Test::Pod 1.41; + +all_pod_files_ok(); diff --git a/xt/CLI.pm b/xt/CLI.pm new file mode 100644 index 0000000..93d50c8 --- /dev/null +++ b/xt/CLI.pm @@ -0,0 +1,61 @@ +package xt::CLI; +use strict; +use base qw(Exporter); +our @EXPORT = qw(run cli); + +use Test::Requires qw( Capture::Tiny File::pushd ); + +sub cli { + my $cli = Carton::CLI::Tested->new; + $cli->dir( Path::Tiny->tempdir(CLEANUP => !$ENV{NO_CLEANUP}) ); + warn "Temp directory: ", $cli->dir, "\n" if $ENV{NO_CLEANUP}; + $cli; +} + +package Carton::CLI::Tested; +use Carton::CLI; +use Capture::Tiny qw(capture); +use File::pushd (); +use Path::Tiny; + +$Carton::CLI::UseSystem = 1; + +use Class::Tiny qw( dir stdout stderr exit_code ); + +sub write_file { + my($self, $file, @args) = @_; + $self->dir->child($file)->spew(@args); +} + +sub write_cpanfile { + my($self, @args) = @_; + $self->write_file(cpanfile => @args); +} + +sub run_in_dir { + my($self, $dir, @args) = @_; + local $self->{dir} = $self->dir->child($dir); + $self->run(@args); +} + +sub run { + my($self, @args) = @_; + + my $pushd = File::pushd::pushd $self->dir; + + my @capture = capture { + my $code = eval { Carton::CLI->new->run(@args) }; + $self->exit_code($@ ? 255 : $code); + }; + + $self->stdout($capture[0]); + $self->stderr($capture[1]); +} + +sub clean_local { + my $self = shift; + $self->dir->child("local")->remove_tree({ safe => 0 }); +} + +1; + diff --git a/xt/cli/bundle.t b/xt/cli/bundle.t new file mode 100644 index 0000000..aa84f24 --- /dev/null +++ b/xt/cli/bundle.t @@ -0,0 +1,19 @@ +use strict; +use Test::More; +use xt::CLI; + +{ + my $app = cli(); + + $app->write_cpanfile(<<EOF); +requires 'Try::Tiny', '== 0.12'; +EOF + + $app->run("install"); + $app->run("bundle"); + + ok -f ($app->dir . "/vendor/cache/authors/id/D/DO/DOY/Try-Tiny-0.12.tar.gz"); +} + +done_testing; + diff --git a/xt/cli/check.t b/xt/cli/check.t new file mode 100644 index 0000000..dd00ed9 --- /dev/null +++ b/xt/cli/check.t @@ -0,0 +1,96 @@ +use strict; +use Test::More; +use xt::CLI; + +subtest 'carton check fails when there is no lock' => sub { + my $app = cli(); + $app->write_cpanfile(<<EOF); +requires 'Try::Tiny', '== 0.11'; +EOF + + $app->run("check"); + like $app->stderr, qr/find cpanfile\.snapshot/; +}; + +subtest 'carton install and check' => sub { + my $app = cli(); + $app->write_cpanfile(<<EOF); +requires 'Try::Tiny', '== 0.11'; +EOF + + $app->run("install"); + + $app->run("check"); + like $app->stdout, qr/are satisfied/; + + $app->run("list"); + like $app->stdout, qr/Try-Tiny-0\.11/; + + $app->write_cpanfile(<<EOF); +requires 'Try::Tiny', '0.16'; +EOF + + $app->run("check"); + like $app->stdout, qr/not satisfied/; + + TODO: { + local $TODO = 'exec does not verify lock'; + $app->run("exec", "perl", "use Try::Tiny"); + like $app->stderr, qr/\.snapshot/; + } + + $app->run("install"); + + $app->run("check"); + like $app->stdout, qr/are satisfied/; + + $app->run("list"); + like $app->stdout, qr/Try-Tiny-0\.\d\d/; + + $app->write_cpanfile(<<EOF); +requires 'Try::Tiny', '10.00'; +EOF + + $app->run("check"); + like $app->stdout, qr/not satisfied/; + + $app->run("install"); + like $app->stderr, qr/failed/; + + $app->run("check"); + like $app->stdout, qr/not satisfied/; +}; + +subtest 'detect unused modules' => sub { + my $app = cli; + $app->write_cpanfile("requires 'Try::Tiny';"); + + $app->run("install"); + $app->write_cpanfile(""); + + + TODO: { + local $TODO = "Can't detect superflous modules"; + $app->run("install"); + $app->run("list"); + is $app->stdout, ""; + + $app->run("check"); + like $app->stdout, qr/unused/; + } +}; + +subtest 'detect downgrade' => sub { + my $app = cli; + $app->write_cpanfile("requires 'URI';"); + $app->run("install"); + + $app->write_cpanfile("requires 'URI', '== 1.59';"); + $app->run("check"); + + like $app->stdout, qr/not satisfied/; + like $app->stdout, qr/URI has version .* Needs == 1\.59/; +}; + +done_testing; + diff --git a/xt/cli/cpanfile.t b/xt/cli/cpanfile.t new file mode 100644 index 0000000..b663b37 --- /dev/null +++ b/xt/cli/cpanfile.t @@ -0,0 +1,44 @@ +use strict; +use Test::More; +use xt::CLI; + +subtest 'carton install --cpanfile' => sub { + my $app = cli(); + $app->write_file('cpanfile.foo', <<EOF); +requires 'Try::Tiny', '== 0.11'; +EOF + $app->run("install", "--cpanfile", "cpanfile.foo"); + $app->run("check", "--cpanfile", "cpanfile.foo"); + + ok !$app->dir->child('cpanfile.snapshot')->exists; + ok $app->dir->child('cpanfile.foo.snapshot')->exists; + + like $app->stdout, qr/are satisfied/; + + local $ENV{PERL_CARTON_CPANFILE} = $app->dir->child('cpanfile.foo')->absolute; + + $app->run("list"); + like $app->stdout, qr/Try-Tiny-0\.11/; + + $app->run("exec", "perl", "-e", "use Try::Tiny\ 1"); + like $app->stderr, qr/Try::Tiny .* 0\.11/; +}; + +subtest 'PERL_CARTON_CPANFILE' => sub { + my $app = cli(); + + local $ENV{PERL_CARTON_CPANFILE} = $app->dir->child('cpanfile.foo')->absolute; + + $app->write_file('cpanfile.foo', <<EOF); +requires 'Try::Tiny', '== 0.11'; +EOF + + $app->run("install"); + $app->run("list"); + + like $app->stdout, qr/Try-Tiny-0\.11/; + ok $app->dir->child('cpanfile.foo.snapshot')->exists; +}; + +done_testing; + diff --git a/xt/cli/deployment.t b/xt/cli/deployment.t new file mode 100644 index 0000000..175f87a --- /dev/null +++ b/xt/cli/deployment.t @@ -0,0 +1,26 @@ +use strict; +use Test::More; +use xt::CLI; + +{ + my $app = cli(); + $app->write_cpanfile(<<EOF); +requires 'Try::Tiny', '== 0.11'; +EOF + + $app->run("install", "--deployment"); + like $app->stderr, qr/deployment requires cpanfile\.snapshot/; + + $app->run("install"); + $app->clean_local; + + $app->run("install", "--deployment"); + $app->run("list"); + like $app->stdout, qr/Try-Tiny-0\.11/; + + $app->run("exec", "perl", "-e", "use Try::Tiny 2;"); + like $app->stderr, qr/Try::Tiny.* version 0\.11/; +} + +done_testing; + diff --git a/xt/cli/deps_phase.t b/xt/cli/deps_phase.t new file mode 100644 index 0000000..862dc76 --- /dev/null +++ b/xt/cli/deps_phase.t @@ -0,0 +1,29 @@ +use strict; +use Test::More; +use xt::CLI; + +{ + my $app = cli(); + + $app->write_cpanfile(<<EOF); +on test => sub { + requires 'Test::NoWarnings'; + recommends 'Test::Pretty'; +}; +on develop => sub { + requires 'Path::Tiny'; +}; +EOF + + $app->run("install"); + + $app->run("list"); + like $app->stdout, qr/Test-NoWarnings/; + like $app->stdout, qr/Path-Tiny/; + unlike $app->stdout, qr/Test-Pretty/; +} + +done_testing; + + + diff --git a/xt/cli/exec.t b/xt/cli/exec.t new file mode 100644 index 0000000..233f790 --- /dev/null +++ b/xt/cli/exec.t @@ -0,0 +1,84 @@ +use strict; +use Test::More; +use xt::CLI; + +subtest 'carton exec without a command', sub { + my $app = cli(); + $app->write_cpanfile(''); + $app->run("install"); + $app->run("exec"); + like $app->stderr, qr/carton exec needs a command/; + is $app->exit_code, 255; +}; + +subtest 'exec without cpanfile', sub { + my $app = cli(); + $app->run("exec", "perl", "-e", 1); + like $app->stderr, qr/Can't locate cpanfile/; + is $app->exit_code, 255; +}; + +subtest 'exec without a snapshot', sub { + my $app = cli(); + $app->write_cpanfile(); + $app->run("exec", "perl", "-e", 1); + like $app->stderr, qr/cpanfile\.snapshot/; + is $app->exit_code, 255; +}; + +subtest 'carton exec', sub { + my $app = cli(); + $app->write_cpanfile(''); + $app->run("install"); + + TODO: { + local $TODO = "exec now does not strip site_perl"; + $app->run("exec", "perl", "-e", "use Try::Tiny"); + like $app->stderr, qr/Can't locate Try\/Tiny.pm/; + } + + $app->write_cpanfile(<<EOF); +requires 'Try::Tiny', '== 0.11'; +EOF + + $app->run("install"); + + $app->run("exec", "--", "perl", "-e", 'use Try::Tiny; print $Try::Tiny::VERSION, "\n"'); + like $app->stdout, qr/0\.11/; + + $app->run("exec", "perl", "-e", 'use Try::Tiny; print $Try::Tiny::VERSION, "\n"'); + like $app->stdout, qr/0\.11/, "No need for -- as well"; + + $app->run("exec", "perl", "-MTry::Tiny", "-e", 'print $Try::Tiny::VERSION, "\n"'); + like $app->stdout, qr/0\.11/; + + $app->write_cpanfile(<<EOF); +requires 'Try::Tiny'; +requires 'App::Ack', '== 2.02'; +EOF + + $app->run("install"); + $app->run("exec", "--", "ack", "--version"); + + like $app->stdout, qr/ack 2\.02/; +}; + +subtest 'carton exec perl -Ilib', sub { + my $app = cli(); + $app->write_cpanfile(''); + $app->run("install"); + + $app->dir->child("lib")->mkpath; + $app->dir->child("lib/FooBarBaz.pm")->spew("package FooBarBaz; 1"); + + $app->run("exec", "perl", "-Ilib", "-e", 'use FooBarBaz; print "foo"'); + like $app->stdout, qr/foo/; + unlike $app->stderr, qr/exec -Ilib is deprecated/; + + $app->run("exec", "-Ilib", "perl", "-e", 'print "foo"'); + like $app->stdout, qr/foo/; + like $app->stderr, qr/exec -Ilib is deprecated/; +}; + +done_testing; + diff --git a/xt/cli/freeze.t b/xt/cli/freeze.t new file mode 100644 index 0000000..f02a2c3 --- /dev/null +++ b/xt/cli/freeze.t @@ -0,0 +1,24 @@ +use strict; +use Test::More; +use xt::CLI; + +{ + my $app = cli(); + + $app->write_cpanfile(<<EOF); +requires 'Try::Tiny', '== 0.11'; +EOF + + $app->run("install"); + $app->run("list"); + like $app->stdout, qr/Try-Tiny-0\.11/; + + $app->clean_local; + + $app->run("install"); + $app->run("list"); + like $app->stdout, qr/Try-Tiny-0\.11/; +} + +done_testing; + diff --git a/xt/cli/help.t b/xt/cli/help.t new file mode 100644 index 0000000..0ad0896 --- /dev/null +++ b/xt/cli/help.t @@ -0,0 +1,25 @@ +use strict; +use Test::More; +use xt::CLI; + +{ + my $app = cli(); + $app->run("help"); + like $app->stdout, qr/Carton - Perl module/; + + $app->run("-h"); + like $app->stdout, qr/Carton - Perl module/; + + $app->run("help", "install"); + like $app->stdout, qr/Install the dependencies/; + + $app->run("install", "-h"); + like $app->stdout, qr/Install the dependencies/; + + $app->run("help", "foobarbaz"); + is $app->stdout, ''; + like $app->stderr, qr/No documentation found/; +} + +done_testing; + diff --git a/xt/cli/install.t b/xt/cli/install.t new file mode 100644 index 0000000..926f3de --- /dev/null +++ b/xt/cli/install.t @@ -0,0 +1,56 @@ +use strict; +use Test::More; +use xt::CLI; + +subtest 'carton install with version range' => sub { + my $app = cli(); + $app->write_cpanfile(<<EOF); +requires 'CPAN::Test::Dummy::Perl5::Deps::VersionRange'; +EOF + + $app->run("install"); + $app->run("tree"); + like $app->stdout, qr/Try::Tiny/; + unlike $app->stderr, qr/Could not parse snapshot file/; +}; + +subtest 'meta info for ancient modules' => sub { + my $app = cli(); + $app->write_cpanfile(<<EOF); +requires 'Algorithm::Diff'; +EOF + + $app->run("install"); + $app->run("list"); + + like $app->stdout, qr/Algorithm-Diff/; +}; + +subtest 'meta info for modules with version->declare' => sub { + my $app = cli(); + $app->write_cpanfile(<<EOF); +requires 'CPAN::Test::Dummy::Perl5::VersionDeclare', 'v0.0.1'; +EOF + + $app->run("install"); + $app->run("check"); + + like $app->stdout, qr/are satisfied/; + unlike $app->stderr, qr/is not installed/; +}; + +subtest 'meta info for modules with qv()' => sub { + my $app = cli(); + $app->write_cpanfile(<<EOF); +requires 'CPAN::Test::Dummy::Perl5::VersionQV', 'v0.1.0'; +EOF + + $app->run("install"); + $app->run("check"); + + like $app->stdout, qr/are satisfied/; + unlike $app->stderr, qr/is not installed/; +}; + +done_testing; + diff --git a/xt/cli/json_pp.t b/xt/cli/json_pp.t new file mode 100644 index 0000000..0c64395 --- /dev/null +++ b/xt/cli/json_pp.t @@ -0,0 +1,23 @@ +use strict; +use Test::More; +use xt::CLI; + +plan skip_all => "perl <= 5.14" if $] >= 5.015; + +{ + my $app = cli(); + + $app->write_cpanfile(<<EOF); +requires 'JSON'; +requires 'CPAN::Meta', '2.12'; +EOF + + $app->run("install"); + $app->clean_local; + + $app->run("install", "--deployment"); + unlike $app->stderr, qr/JSON::PP is not in range/; +} + +done_testing; + diff --git a/xt/cli/mirror.t b/xt/cli/mirror.t new file mode 100644 index 0000000..5ab9a29 --- /dev/null +++ b/xt/cli/mirror.t @@ -0,0 +1,38 @@ +use strict; +use Test::More; +use xt::CLI; + +my $cwd = Path::Tiny->cwd; + +{ + my $app = cli(); + + $app->write_cpanfile(<<EOF); +requires 'Hash::MultiValue'; +EOF + + local $ENV{PERL_CARTON_MIRROR} = "$cwd/xt/mirror"; + $app->run("install"); + + $app->run("list"); + like $app->stdout, qr/^Hash-MultiValue-0.08/m; +} + +{ + # fallback to CPAN + my $app = cli(); + $app->write_cpanfile(<<EOF); +requires 'PSGI'; +EOF + + local $ENV{PERL_CARTON_MIRROR} = "$cwd/xt/mirror"; + $app->run("install"); + + $app->run("list"); + like $app->stdout, qr/^PSGI-/m; +} + +done_testing; + + + diff --git a/xt/cli/mismatch.t b/xt/cli/mismatch.t new file mode 100644 index 0000000..11d5af4 --- /dev/null +++ b/xt/cli/mismatch.t @@ -0,0 +1,24 @@ +use strict; +use Test::More; +use xt::CLI; + +{ + my $app = cli(); + + $app->write_cpanfile(<<EOF); +requires 'Data::Dumper' => '== 2.139'; +requires 'Test::Differences' => '== 0.61'; +EOF + + $app->run("install"); + $app->run("list"); + + like $app->stdout, qr/Data-Dumper-2\.139/; + like $app->stdout, qr/Test-Differences-0\.61/; + + $app->run("check"); + like $app->stdout, qr/are satisfied/; +} + +done_testing; + diff --git a/xt/cli/no_cpanfile.t b/xt/cli/no_cpanfile.t new file mode 100644 index 0000000..e12d2c3 --- /dev/null +++ b/xt/cli/no_cpanfile.t @@ -0,0 +1,13 @@ +use strict; +use Test::More; +use xt::CLI; + +{ + my $app = cli(); + $app->run("install"); + like $app->stderr, qr/Can't locate cpanfile/; + is $app->exit_code, 255; +} + +done_testing; + diff --git a/xt/cli/perl.t b/xt/cli/perl.t new file mode 100644 index 0000000..a7e0143 --- /dev/null +++ b/xt/cli/perl.t @@ -0,0 +1,23 @@ +use strict; +use Test::More; +use xt::CLI; + +{ + my $app = cli(); + + $app->write_cpanfile(<<EOF); +requires 'perl', '5.8.5'; +requires 'Hash::MultiValue'; +EOF + + $app->run("install"); + like $app->stdout, qr/Complete/; + + $app->run("list"); + like $app->stdout, qr/Hash-MultiValue-/; +} + +done_testing; + + + diff --git a/xt/cli/snapshot.t b/xt/cli/snapshot.t new file mode 100644 index 0000000..b036af7 --- /dev/null +++ b/xt/cli/snapshot.t @@ -0,0 +1,65 @@ +use strict; +use Test::More; +use xt::CLI; + +subtest 'snapshot file has canonical representation' => sub { + my $app = cli(); + $app->write_cpanfile(<<EOF); +requires 'Try::Tiny', '== 0.11'; +requires 'Getopt::Long', '2.41'; +EOF + + $app->run("install"); + + my $content = $app->dir->child('cpanfile.snapshot')->slurp; + for (1..3) { + $app->dir->child('cpanfile.snapshot')->remove; + $app->run("install"); + is $content, $app->dir->child('cpanfile.snapshot')->slurp; + } +}; + +subtest 'Bad snapshot version' => sub { + my $app = cli(); + $app->write_cpanfile(''); + $app->write_file('cpanfile.snapshot', <<EOF); +# carton snapshot format: version 111 +EOF + + $app->run("install"); + like $app->stderr, qr/Could not parse/; +}; + +subtest 'Bad snapshot file' => sub { + my $app = cli(); + $app->write_cpanfile(''); + $app->write_file('cpanfile.snapshot', <<EOF); +# carton snapshot format: version 1.0 +DISTRIBUTIONS + Foo-Bar-1 + unknown: foo +EOF + + $app->run("install"); + like $app->stderr, qr/Could not parse/; +}; + +subtest 'snapshot file support separate CRLF' => sub { + my $app = cli(); + $app->write_cpanfile(<<EOF); +requires 'Try::Tiny', '== 0.11'; +requires 'Getopt::Long', '2.41'; +EOF + + $app->run("install"); + + my $content = $app->dir->child('cpanfile.snapshot')->slurp; + $content =~ s/\n/\r\n/g; + $app->write_file('cpanfile.snapshot', $content); + + $app->run("install"); + ok !$app->stderr; +}; + +done_testing; + diff --git a/xt/cli/subdir.t b/xt/cli/subdir.t new file mode 100644 index 0000000..84d244f --- /dev/null +++ b/xt/cli/subdir.t @@ -0,0 +1,25 @@ +use strict; +use Test::More; +use xt::CLI; + +subtest 'carton exec in subdir', sub { + my $app = cli(); + $app->write_cpanfile(<<EOF); +requires 'Try::Tiny'; +EOF + $app->run('install'); + + $app->dir->child('x')->mkpath; + + $app->run_in_dir('x' => 'list'); + like $app->stdout, qr/Try-Tiny/; + + $app->run_in_dir('x' => 'check'); + like $app->stdout, qr/are satisfied/; + + $app->run_in_dir('x' => 'install'); + like $app->stdout, qr/Complete/; + unlike $app->stderr, qr/failed/; +}; + +done_testing; diff --git a/xt/cli/tree.t b/xt/cli/tree.t new file mode 100644 index 0000000..bc7ce8c --- /dev/null +++ b/xt/cli/tree.t @@ -0,0 +1,23 @@ +use strict; +use Test::More; +use xt::CLI; + +{ + my $app = cli(); + + $app->write_cpanfile(<<EOF); +requires 'HTML::Parser'; +EOF + + $app->run("install"); + $app->run("tree"); + + is $app->exit_code, 0; + like $app->stdout, qr/^HTML::Parser \(HTML-Parser-/m; + like $app->stdout, qr/^ HTML::Tagset \(HTML-Tagset-/m; +} + +done_testing; + + + diff --git a/xt/cli/update.t b/xt/cli/update.t new file mode 100644 index 0000000..22745bd --- /dev/null +++ b/xt/cli/update.t @@ -0,0 +1,78 @@ +use strict; +use Test::More; +use xt::CLI; + +subtest 'carton update NonExistentModule' => sub { + my $app = cli(); + + $app->write_cpanfile(<<EOF); +requires 'Try::Tiny', '== 0.09'; +EOF + + $app->run("install"); + $app->run("update", "XYZ"); + like $app->stderr, qr/Could not find module XYZ/; +}; + +subtest 'carton update upgrades a dist' => sub { + my $app = cli(); + + $app->write_cpanfile(<<EOF); +requires 'Try::Tiny', '== 0.09'; +EOF + + $app->run("install"); + $app->run("list"); + like $app->stdout, qr/Try-Tiny-0\.09/; + + $app->write_cpanfile(<<EOF); +requires 'Try::Tiny', '>= 0.09, <= 0.12'; +EOF + + $app->run("install"); + $app->run("check"); + like $app->stdout, qr/are satisfied/; + + $app->run("list"); + like $app->stdout, qr/Try-Tiny-0\.09/; + + $app->run("update", "Try::Tiny"); + like $app->stdout, qr/installed Try-Tiny-0\.12.*upgraded from 0\.09/; + + $app->run("check"); + like $app->stdout, qr/are satisfied/; + + $app->run("list"); + like $app->stdout, qr/Try-Tiny-0\.12/; +}; + +subtest 'downgrade a distribution' => sub { + my $app = cli(); + + $app->write_cpanfile(<<EOF); +requires 'Try::Tiny', '0.16'; +EOF + $app->run("install"); + $app->run("list"); + like $app->stdout, qr/Try-Tiny-0\.\d\d/; + + $app->write_cpanfile(<<EOF); +requires 'Try::Tiny', '== 0.09'; +EOF + $app->run("update"); + $app->run("list"); + like $app->stdout, qr/Try-Tiny-0\.09/; + + TODO: { + local $TODO = 'collecting wrong install info'; + $app->write_cpanfile(<<EOF); +requires 'Try::Tiny', '0.09'; +EOF + $app->run("install"); + $app->run("list"); + like $app->stdout, qr/Try-Tiny-0\.09/; + } +}; + +done_testing; + diff --git a/xt/cli/version.t b/xt/cli/version.t new file mode 100644 index 0000000..d99b9d6 --- /dev/null +++ b/xt/cli/version.t @@ -0,0 +1,12 @@ +use strict; +use Test::More; + +use xt::CLI; + +my $app = cli(); +$app->run("version"); + +like $app->stdout, qr/carton $Carton::VERSION/; + +done_testing; + diff --git a/xt/cli/without.t b/xt/cli/without.t new file mode 100644 index 0000000..d9c51f7 --- /dev/null +++ b/xt/cli/without.t @@ -0,0 +1,67 @@ +use strict; +use Test::More; +use xt::CLI; + +subtest 'carton install --without develop' => sub { + my $app = cli(); + $app->write_cpanfile(<<EOF); +requires 'Try::Tiny'; + +on 'develop' => sub { + requires 'Hash::MultiValue', '== 0.14'; +}; +EOF + + $app->run("install"); + $app->run("list"); + like $app->stdout, qr/Try-Tiny-/; + like $app->stdout, qr/Hash-MultiValue-0\.14/; + + $app->run("exec", "perl", "-e", "use Hash::MultiValue\ 1"); + like $app->stderr, qr/Hash::MultiValue .* version 0.14/; + + $app->clean_local; + + $app->run("install", "--without", "develop"); + $app->run("list"); + like $app->stdout, qr/Try-Tiny-/; + + TODO: { + local $TODO = "--without is not remembered for list"; + unlike $app->stdout, qr/Hash-MultiValue-/; + } + + $app->run("exec", "perl", "-e", "use Hash::MultiValue\ 1"); + unlike $app->stderr, qr/Hash::MultiValue .* version 0.14/; +}; + +subtest 'without features' => sub { + my $app = cli(); + $app->write_cpanfile(<<EOF); +requires 'Try::Tiny'; + +feature 'stream' => sub { + requires 'Stream::Buffered', '== 0.01'; +}; +EOF + + $app->run("install"); + $app->run("list"); + like $app->stdout, qr/Stream-Buffered-0\.01/; + + $app->clean_local; + + $app->run("install", "--deployment"); + $app->run("exec", "perl", "-e", "use Stream::Buffered 1"); + like $app->stderr, qr/Stream::Buffered .* version 0\.01/; + + $app->clean_local; + + $app->run("install", "--without", "stream"); + + $app->run("exec", "perl", "-e", "use Stream::Buffered 1"); + unlike $app->stderr, qr/Stream::Buffered .* version 0\.01/; +}; + +done_testing; + diff --git a/xt/mirror/authors/id/M/MI/MIYAGAWA/Hash-MultiValue-0.08.tar.gz b/xt/mirror/authors/id/M/MI/MIYAGAWA/Hash-MultiValue-0.08.tar.gz Binary files differnew file mode 100644 index 0000000..bb431bc --- /dev/null +++ b/xt/mirror/authors/id/M/MI/MIYAGAWA/Hash-MultiValue-0.08.tar.gz diff --git a/xt/mirror/modules/02packages.details.txt b/xt/mirror/modules/02packages.details.txt new file mode 100644 index 0000000..40ed304 --- /dev/null +++ b/xt/mirror/modules/02packages.details.txt @@ -0,0 +1,25 @@ +File: 02packages.details.txt +URL: http://www.perl.com/CPAN/modules/02packages.details.txt +Description: Package names found in carton.lock +Columns: package name, version, path +Intended-For: Automated fetch routines, namespace documentation. +Written-By: Carton v0.9.0 +Line-Count: 16 +Last-Updated: Wed Jun 29 22:54:55 2011 + +CGI 3.55 M/MA/MARKSTOS/CGI.pm-3.55.tar.gz +CGI::Apache 1.01 M/MA/MARKSTOS/CGI.pm-3.55.tar.gz +CGI::Carp 3.51 M/MA/MARKSTOS/CGI.pm-3.55.tar.gz +CGI::Cookie 1.30 M/MA/MARKSTOS/CGI.pm-3.55.tar.gz +CGI::Fast 1.08 M/MA/MARKSTOS/CGI.pm-3.55.tar.gz +CGI::Pretty 3.46 M/MA/MARKSTOS/CGI.pm-3.55.tar.gz +CGI::Push 1.05 M/MA/MARKSTOS/CGI.pm-3.55.tar.gz +CGI::Switch 1.01 M/MA/MARKSTOS/CGI.pm-3.55.tar.gz +CGI::Util 3.53 M/MA/MARKSTOS/CGI.pm-3.55.tar.gz +CGITempFile undef M/MA/MARKSTOS/CGI.pm-3.55.tar.gz +FCGI 0.73 F/FL/FLORA/FCGI-0.73.tar.gz +FCGI::Stream undef F/FL/FLORA/FCGI-0.73.tar.gz +Fh undef M/MA/MARKSTOS/CGI.pm-3.55.tar.gz +Hash::MultiValue 0.08 M/MI/MIYAGAWA/Hash-MultiValue-0.08.tar.gz +MultipartBuffer undef M/MA/MARKSTOS/CGI.pm-3.55.tar.gz +Try::Tiny 0.09 D/DO/DOY/Try-Tiny-0.09.tar.gz diff --git a/xt/mirror/modules/02packages.details.txt.gz b/xt/mirror/modules/02packages.details.txt.gz Binary files differnew file mode 100644 index 0000000..1535767 --- /dev/null +++ b/xt/mirror/modules/02packages.details.txt.gz |