summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Changes248
-rw-r--r--LICENSE379
-rw-r--r--MANIFEST66
-rw-r--r--META.json96
-rw-r--r--META.yml62
-rw-r--r--Makefile.PL73
-rw-r--r--README165
-rw-r--r--cpanfile32
-rw-r--r--dist.ini3
-rw-r--r--lib/Carton.pm179
-rw-r--r--lib/Carton/Builder.pm114
-rw-r--r--lib/Carton/CLI.pm396
-rw-r--r--lib/Carton/CPANfile.pm44
-rw-r--r--lib/Carton/Dependency.pm21
-rw-r--r--lib/Carton/Dist.pm37
-rw-r--r--lib/Carton/Dist/Core.pm23
-rw-r--r--lib/Carton/Doc/Bundle.pod20
-rw-r--r--lib/Carton/Doc/Check.pod24
-rw-r--r--lib/Carton/Doc/Exec.pod19
-rw-r--r--lib/Carton/Doc/FAQ.pod112
-rw-r--r--lib/Carton/Doc/Fatpack.pod15
-rw-r--r--lib/Carton/Doc/Install.pod95
-rw-r--r--lib/Carton/Doc/List.pod23
-rw-r--r--lib/Carton/Doc/Show.pod12
-rw-r--r--lib/Carton/Doc/Tree.pod13
-rw-r--r--lib/Carton/Doc/Update.pod40
-rw-r--r--lib/Carton/Doc/Upgrading.pod48
-rw-r--r--lib/Carton/Doc/Version.pod11
-rw-r--r--lib/Carton/Environment.pm100
-rw-r--r--lib/Carton/Error.pm42
-rw-r--r--lib/Carton/Index.pm68
-rw-r--r--lib/Carton/Mirror.pm23
-rw-r--r--lib/Carton/Package.pm12
-rw-r--r--lib/Carton/Packer.pm97
-rw-r--r--lib/Carton/Snapshot.pm191
-rw-r--r--lib/Carton/Snapshot/Emitter.pm30
-rw-r--r--lib/Carton/Snapshot/Parser.pm126
-rw-r--r--lib/Carton/Tree.pm69
-rw-r--r--lib/Carton/Util.pm31
-rwxr-xr-xscript/carton27
-rw-r--r--t/release-pod-syntax.t14
-rw-r--r--xt/CLI.pm61
-rw-r--r--xt/cli/bundle.t19
-rw-r--r--xt/cli/check.t96
-rw-r--r--xt/cli/cpanfile.t44
-rw-r--r--xt/cli/deployment.t26
-rw-r--r--xt/cli/deps_phase.t29
-rw-r--r--xt/cli/exec.t84
-rw-r--r--xt/cli/freeze.t24
-rw-r--r--xt/cli/help.t25
-rw-r--r--xt/cli/install.t56
-rw-r--r--xt/cli/json_pp.t23
-rw-r--r--xt/cli/mirror.t38
-rw-r--r--xt/cli/mismatch.t24
-rw-r--r--xt/cli/no_cpanfile.t13
-rw-r--r--xt/cli/perl.t23
-rw-r--r--xt/cli/snapshot.t65
-rw-r--r--xt/cli/subdir.t25
-rw-r--r--xt/cli/tree.t23
-rw-r--r--xt/cli/update.t78
-rw-r--r--xt/cli/version.t12
-rw-r--r--xt/cli/without.t67
-rw-r--r--xt/mirror/authors/id/M/MI/MIYAGAWA/Hash-MultiValue-0.08.tar.gzbin0 -> 47374 bytes
-rw-r--r--xt/mirror/modules/02packages.details.txt25
-rw-r--r--xt/mirror/modules/02packages.details.txt.gzbin0 -> 520 bytes
65 files changed, 4080 insertions, 0 deletions
diff --git a/Changes b/Changes
new file mode 100644
index 0000000..addbcce
--- /dev/null
+++ b/Changes
@@ -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
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..7b5e320
--- /dev/null
+++ b/LICENSE
@@ -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);
diff --git a/README b/README
new file mode 100644
index 0000000..32be945
--- /dev/null
+++ b/README
@@ -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
new file mode 100644
index 0000000..bb431bc
--- /dev/null
+++ b/xt/mirror/authors/id/M/MI/MIYAGAWA/Hash-MultiValue-0.08.tar.gz
Binary files differ
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
new file mode 100644
index 0000000..1535767
--- /dev/null
+++ b/xt/mirror/modules/02packages.details.txt.gz
Binary files differ