├── README.md ├── bin ├── common.sh ├── compile ├── detect ├── release └── steps │ └── hooks │ ├── post_compile │ └── pre_compile └── support ├── perl-build-extras.sh └── perl-build.sh /README.md: -------------------------------------------------------------------------------- 1 | Heroku buildpack: Perl 2 | ====================== 3 | 4 | This is a Heroku buildpack that runs any PSGI based web applications using Starman. 5 | 6 | Usage 7 | ----- 8 | 9 | Example usage: 10 | 11 | $ ls 12 | cpanfile 13 | app.psgi 14 | lib/ 15 | 16 | $ cat cpanfile 17 | requires 'Plack', '1.0000'; 18 | requires 'DBI', '1.6'; 19 | 20 | $ heroku create --buildpack http://github.com/pnu/heroku-buildpack-perl.git 21 | 22 | $ git push heroku master 23 | ... 24 | -----> Heroku receiving push 25 | -----> Fetching custom buildpack 26 | -----> Perl/PSGI app detected 27 | -----> Installing dependencies 28 | 29 | The buildpack will detect that your app has an `app.psgi` in the root. 30 | 31 | Libraries 32 | --------- 33 | 34 | Dependencies can be declared using `cpanfile` or traditional `Makefile.PL` and `Build.PL`, and the buildpack will install these dependencies using [cpanm](http://cpanmin.us) into `./local` directory. 35 | 36 | Perl version 37 | ------------ 38 | 39 | Perl versions can be specified with `.perl-version` file a la plenv. 40 | 41 | $ echo 5.18.2 > .perl-version 42 | $ git add .perl-version 43 | $ git commit -m 'use 5.18.2' 44 | 45 | By default the system perl is used. 46 | 47 | Compiling perl 48 | -------------- 49 | 50 | Script `support/perl-build.sh` can be used to build your own perl versions. 51 | The build script runs on Heroku, and uploads the compiled package to a S3 bucket. 52 | S3 credentials, bucket name and perl version are specified in the environment. 53 | 54 | heroku create your-builder-app-1234 --stack cedar-14 55 | heroku addons:add papertrail --app your-builder-app-1234 56 | heroku config:set AWS_ACCESS_KEY_ID="xxx" AWS_SECRET_ACCESS_KEY="yyy" S3_BUCKET_NAME="heroku-buildpack-perl" HEROKU_STACK="cedar-14" --app your-builder-app-1234 57 | heroku run:detached --size 2x 'curl -sL https://raw.github.com/pnu/heroku-buildpack-perl/master/support/perl-build.sh | PERL_VERSION=5.20.1 bash' --app your-builder-app-1234 58 | [...] 59 | heroku destroy --app your-builder-app-1234 --confirm your-builder-app-1234 60 | 61 | You can use the same build server to build multiple perl versions concurrently. After perl binary has been compiled, use perl-build-extras.sh to create a package of prebuilt libraries for the same perl version. By default the buildpack will vendor this library package to avoid timeouts in slug compilation. 62 | 63 | Prebuilt libraries 64 | ------------------ 65 | 66 | Building all dependecies of a typical Catalyst application for the first time may timeout the slug compilation. However, this is not an issue for incremental builds, as the `./local` directory is cached between builds. 67 | 68 | To avoid the timeout, you can prebuild the libraries, and specify an environment variable `LOCALURL` that points to a tar package. The `./local` directory is replaced with the content of this tar before building rest of the dependecies. 69 | 70 | As an example, here are the steps I've used to bootstrap the libraries for such projects. First upload the `cpanfile` to some url. Github gist works fine for this purpose. You'll also need a S3 bucket for hosting the tar package. After creating the heroku application, but before doing the git push (that would timeout while installing the dependencies), install the dependencies in a one-off process: 71 | 72 | $ heroku config:set AWS_ACCESS_KEY_ID=xxx 73 | $ heroku config:set AWS_SECRET_ACCESS_KEY=yyy 74 | $ heroku config:set S3_BUCKET_NAME=zzz 75 | 76 | $ heroku run 'curl -sL https://raw.github.com/pnu/heroku-vendor-cpanfile/master/build | CPANFILE=https://raw.github.com/gist/3713534/7430a2eab5ac6f959d69aa3042052b417e5d27ac/cpanfile bash' 77 | 78 | $ heroku config:set LOCALURL=http://zzz.s3.amazonaws.com/local-nnn.tar.gz 79 | 80 | After doing the initial push, you can unset the LOCALURL. 81 | 82 | Prebuilt libraries (vulcan) 83 | --------------------------- 84 | 85 | This can also be done with `vulcan`: 86 | 87 | $ vulcan build -v -c "rm /app/bin/make; curl -sL http://cpanmin.us | perl - -L /tmp/local --quiet --notest --installdeps ." -p /tmp/local 88 | 89 | Set the returned url as heroku config LOCALURL. 90 | -------------------------------------------------------------------------------- /bin/common.sh: -------------------------------------------------------------------------------- 1 | error() { 2 | echo " ! $*" >&2 3 | exit 1 4 | } 5 | 6 | status() { 7 | echo "-----> $*" 8 | } 9 | 10 | # sed -l basically makes sed replace and buffer through stdin to stdout 11 | # so you get updates while the command runs and dont wait for the end 12 | # e.g. npm install | indent 13 | indent() { 14 | c='s/^/ /' 15 | case $(uname) in 16 | Darwin) sed -l "$c";; # mac/bsd sed: -l buffers on line boundaries 17 | *) sed -u "$c";; # unix/gnu sed: -u unbuffered (arbitrary) chunks of data 18 | esac 19 | } 20 | 21 | cat_cpanm_build_log() { 22 | test -f $HOME/.cpanm/build.log && cat $HOME/.cpanm/build.log 23 | } 24 | 25 | export_env_dir() { 26 | env_dir=$1 27 | whitelist_regex=${2:-''} 28 | blacklist_regex=${3:-'^(PATH|GIT_DIR|CPATH|CPPATH|LD_PRELOAD|LIBRARY_PATH)$'} 29 | if [ -d "$env_dir" ]; then 30 | for e in $(ls $env_dir); do 31 | echo "$e" | grep -E "$whitelist_regex" | grep -qvE "$blacklist_regex" && 32 | export "$e=$(cat $env_dir/$e)" 33 | : 34 | done 35 | fi 36 | } 37 | -------------------------------------------------------------------------------- /bin/compile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e # fail fast 4 | set -o pipefail # don't ignore exit codes when piping output 5 | 6 | BUILD_DIR=$1 7 | CACHE_DIR=$2 8 | ENV_DIR=$3 9 | BP_DIR=$(cd $(dirname $0); cd ..; pwd) 10 | 11 | source $BP_DIR/bin/common.sh 12 | trap cat_cpanm_build_log ERR 13 | mkdir -p $BUILD_DIR/.profile.d 14 | 15 | export_env_dir $ENV_DIR '^BUILDPACK_' '' 16 | if [ -e "$BUILD_DIR/.perl-version" ]; then 17 | PERL_VERSION=`cat "$BUILD_DIR/.perl-version"` 18 | fi 19 | if [ -n "$STACK" ] && [ "$STACK" != "cedar" ]; then 20 | CACHE_DIR="$CACHE_DIR/perl/$STACK" 21 | PERL_VERSION=${PERL_VERSION-'5.22.0'} 22 | else 23 | CACHE_DIR="$CACHE_DIR/perl" 24 | PERL_VERSION=${PERL_VERSION-'5.20.1'} 25 | fi 26 | PERL_VERSION=${BUILDPACK_PERL_VERSION-$PERL_VERSION} 27 | PERL_LIBS=${BUILDPACK_PERL_LIBS-'extras'} 28 | PERL_URL=${BUILDPACK_PERL_URL:-"https://heroku-buildpack-perl.s3.amazonaws.com/$STACK"} 29 | CPANM_URL=${BUILDPACK_CPANM_URL:-"${PERL_URL%/}/cpanm"} 30 | CPANM_OPT=${BUILDPACK_CPANM_OPT-"--quiet --notest"} 31 | if [ -n "$BUILDPACK_CPAN_MIRROR" ]; then 32 | status "Using CPAN mirror $BUILDPACK_CPAN_MIRROR" 33 | CPANM_OPT="$CPANM_OPT --mirror-only --mirror '$BUILDPACK_CPAN_MIRROR'" 34 | fi 35 | export PERL_CPANM_OPT=$CPANM_OPT 36 | 37 | if [ -n "$PERL_VERSION" ] && [ "$PERL_VERSION" != "system" ]; then 38 | PERL_PACKAGE="${PERL_URL%/}/perl-$PERL_VERSION.tgz" 39 | status "Vendoring $PERL_PACKAGE" 40 | mkdir -p "$BUILD_DIR/vendor/perl" 41 | curl -sL $PERL_PACKAGE | tar xzf - -C "$BUILD_DIR/vendor/perl" &> /dev/null 42 | export PATH="$BUILD_DIR/vendor/perl/bin:$PATH" 43 | echo "export PATH=\"\$HOME/vendor/perl/bin:\$PATH\";" > $BUILD_DIR/.profile.d/buildpack_vendor_perl.sh 44 | for package in $PERL_LIBS; do 45 | PERL_PACKAGE="${PERL_URL%/}/perl-$PERL_VERSION-$package.tgz" 46 | status "Vendoring $PERL_PACKAGE" 47 | curl -sL $PERL_PACKAGE | tar xzf - -C "$BUILD_DIR/vendor/perl" &> /dev/null 48 | done 49 | fi 50 | 51 | CURRENT_PERL_VERSION=$( eval $(perl -V:PERL_.*:); echo "$PERL_REVISION.$PERL_VERSION.$PERL_SUBVERSION" ) 52 | status "Current perl version is $CURRENT_PERL_VERSION" 53 | 54 | cd $BUILD_DIR 55 | 56 | if [ `which sha256sum` ]; then 57 | RELEASE_UUID=`dd if=/dev/urandom bs=1k count=1k 2>/dev/null | sha256sum -b | cut -f1 -d' '` 58 | echo "export RELEASE_UUID=\"$RELEASE_UUID\";" > $BUILD_DIR/.profile.d/buildpack_release_uuid.sh 59 | status "Random RELEASE_UUID=$RELEASE_UUID" 60 | fi 61 | 62 | source $BP_DIR/bin/steps/hooks/pre_compile $ENV_DIR 63 | 64 | if [ -d "$CACHE_DIR/local" ]; then 65 | if [ -f "$CACHE_DIR/.perl-version" ] && [ $(cat $CACHE_DIR/.perl-version) != "$CURRENT_PERL_VERSION" ]; then 66 | status "Perl version changed, not restoring local directory from cache" 67 | else 68 | status "Restoring local directory from cache $CACHE_DIR" 69 | cp -R "$CACHE_DIR/local" . &> /dev/null || true 70 | fi 71 | fi 72 | 73 | if [ -e "$ENV_DIR/LOCALURL" ]; then 74 | LOCALURL=`cat $ENV_DIR/LOCALURL` 75 | status "Extracting prebuilt dependencies from $LOCALURL" 76 | mkdir -p local 77 | curl -sL $LOCALURL | tar -xz -C local 2>&1 | indent 78 | fi 79 | 80 | status "Bootstrapping cpanm and local::lib" 81 | curl -sL $CPANM_URL | perl - --local-lib local App::cpanminus local::lib 2>&1 | indent 82 | eval $(perl -Ilocal/lib/perl5 -Mlocal::lib=local 2> /dev/null) 83 | 84 | if [ -e cpanfile.snapshot ]; then 85 | if ! command -v carton >/dev/null 2>&1; then 86 | status "Installing carton" 87 | perl -S cpanm Carton 2>&1 | indent 88 | fi 89 | status "Installing dependencies (carton)" 90 | perl -S carton install --deployment --cached 2>&1 | indent 91 | rm -rf vendor/cache 2>&1 92 | fi 93 | 94 | status "Installing dependencies (cpanm)" 95 | perl -S cpanm --installdeps . 2>&1 | indent 96 | 97 | status "Installing Starman" 98 | perl -S cpanm Starman 2>&1 | indent 99 | 100 | source $BP_DIR/bin/steps/hooks/post_compile $ENV_DIR 101 | 102 | rm -rf $CACHE_DIR &> /dev/null || true 103 | mkdir -p $CACHE_DIR &> /dev/null || true 104 | if [ -d "local" ]; then 105 | status "Caching local to $CACHE_DIR/" 106 | cp -R local $CACHE_DIR/ 107 | fi 108 | echo $CURRENT_PERL_VERSION >$CACHE_DIR/.perl-version 109 | -------------------------------------------------------------------------------- /bin/detect: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # bin/detect 3 | 4 | echo "perl" 5 | exit 0 6 | -------------------------------------------------------------------------------- /bin/release: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | cat << EOF 4 | --- 5 | addons: 6 | config_vars: 7 | LD_LIBRARY_PATH: /app/local/lib:/app/vendor/lib 8 | PATH: /app/local/bin:/app/vendor/bin:/usr/local/bin:/usr/bin:/bin 9 | PERL5LIB: /app/local/lib/perl5:/app/vendor/lib/perl5 10 | default_process_types: 11 | web: starman --preload-app --port \$PORT 12 | EOF 13 | -------------------------------------------------------------------------------- /bin/steps/hooks/post_compile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ENV_DIR=$1 4 | 5 | if [ -f bin/post_compile ]; then 6 | status "Running post-compile hook" 7 | chmod +x bin/post_compile 8 | bin/post_compile $ENV_DIR 2>&1 | indent 9 | fi 10 | -------------------------------------------------------------------------------- /bin/steps/hooks/pre_compile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ENV_DIR=$1 4 | 5 | if [ -f bin/pre_compile ]; then 6 | status "Running pre-compile hook" 7 | chmod +x bin/pre_compile 8 | bin/pre_compile $ENV_DIR 2>&1 | indent 9 | fi 10 | -------------------------------------------------------------------------------- /support/perl-build-extras.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | 4 | CPANMURL='https://raw.github.com/miyagawa/cpanminus/master/cpanm' 5 | VENDOR_DIR='/app/vendor/perl' 6 | HEROKU_STACK=${HEROKU_STACK-'cedar-14'} 7 | 8 | export AUTOMATED_TESTING=1 ## VM::EC2 to skip network tests 9 | export NONINTERACTIVE_TESTING=1 ## http://www.dagolden.com/index.php/2098/the-annotated-lancaster-consensus/ 10 | 11 | rm -rf $VENDOR_DIR 12 | mkdir -p $VENDOR_DIR 13 | 14 | cd $VENDOR_DIR 15 | curl -sL "$S3_BUCKET_NAME.s3.amazonaws.com/$HEROKU_STACK/perl-$PERL_VERSION.tgz" | tar xzf - 16 | find . -exec touch {} \; 17 | sleep 1 18 | touch . 19 | curl -sL $CPANMURL | bin/perl - --quiet --notest App::cpanminus 20 | bin/cpanm --quiet --installdeps File::HomeDir 21 | bin/cpanm --quiet --notest File::HomeDir ## can't find root's home directory on Heroku 22 | bin/cpanm --quiet --installdeps Term::ReadKey 23 | bin/cpanm --quiet --notest Term::ReadKey 24 | bin/cpanm --quiet --installdeps Data::Dump::Streamer 25 | bin/cpanm --quiet --notest Data::Dump::Streamer ## 2.38 fails on 5.22.0 26 | bin/cpanm --quiet --installdeps LWP::Protocol::https 27 | bin/cpanm --quiet --notest LWP::Protocol::https ## https://rt.cpan.org/Public/Bug/Display.html?id=104150 28 | bin/cpanm --quiet --installdeps HTTP::Server::Simple 29 | bin/cpanm --quiet --notest HTTP::Server::Simple ## 0.50 getaddrinfo / localhost fails on cedar stack 30 | bin/cpanm --quiet Task::Moose MooseX::Daemonize Mouse 31 | bin/cpanm --quiet Dancer Dancer2 Mojolicious Catalyst Catalyst::Devel 32 | bin/cpanm --quiet Plack Starman Twiggy Carton local::lib 33 | bin/cpanm --quiet DBI DBIx::Class DBIx::Class::Schema::Loader DBIx::Class::Migration SQL::Translator 34 | bin/cpanm --quiet CHI Redis DBD::Pg Net::Amazon::S3 35 | bin/cpanm --quiet XML::LibXML MIME::Types XML::Atom XML::RSS 36 | 37 | find . -newercm . | tar czf ~/perl-$PERL_VERSION-extras.tgz --no-recursion --files-from=/dev/stdin 38 | cd ~ 39 | 40 | git clone https://github.com/s3tools/s3cmd 41 | cd s3cmd 42 | git checkout v1.5.0-beta1 43 | cat >~/.s3cfg <~/.s3cfg <