├── repo └── .gitignore ├── recipe └── .gitignore ├── archive └── .gitignore ├── ssl ├── .gitignore ├── generate-certs.sh └── openssl.cnf ├── mt ├── mt-expose.yml ├── ldap.yml ├── memcached.yml ├── cgi.yml ├── cgi-bin-mt.htaccess ├── ftpd.yml ├── httpd │ ├── devcontainer │ │ ├── etc │ │ │ └── mt-xdebug.ini │ │ └── build-script │ │ │ └── common-debian.sh │ ├── Dockerfile │ ├── build-script │ │ ├── php.sh │ │ └── apache2.sh │ └── Dockerfile.devcontainer ├── mt │ ├── Dockerfile │ ├── devcontainer │ │ ├── bin │ │ │ └── invoke-perl-with-mt-libs │ │ └── build-script │ │ │ └── common-debian.sh │ ├── build-script │ │ └── apache2.sh │ └── Dockerfile.devcontainer ├── mysql │ └── conf.d │ │ └── mt.cnf ├── mailpit.yml ├── oracle.yml ├── psgi.yml ├── plackup-mt ├── mysql.yml ├── mt-watcher │ └── Dockerfile ├── docker-entrypoint.sh ├── mt-watcher.pl └── common.yml ├── .gitignore ├── bin ├── teardown-environment ├── extract-archive-on-docker ├── git-clone ├── extract-archive ├── local │ └── lib │ │ └── perl5 │ │ ├── Config │ │ └── Tiny.pm │ │ └── YAML │ │ └── Tiny.pm └── setup-environment ├── .devcontainer ├── post-create.sh └── devcontainer.json ├── mt-config.cgi-original ├── README.ja.md ├── README.md ├── Vagrantfile ├── Makefile └── CHANGELOG.md /repo/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | -------------------------------------------------------------------------------- /recipe/.gitignore: -------------------------------------------------------------------------------- 1 | *.yml 2 | *.yaml 3 | -------------------------------------------------------------------------------- /archive/.gitignore: -------------------------------------------------------------------------------- 1 | *.zip 2 | *.tar.* 3 | *.tgz 4 | -------------------------------------------------------------------------------- /ssl/.gitignore: -------------------------------------------------------------------------------- 1 | certs/ 2 | index.txt 3 | serial 4 | -------------------------------------------------------------------------------- /mt/mt-expose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | mt: 3 | ports: 4 | - "${MT_EXPOSE_PORT}:80" 5 | -------------------------------------------------------------------------------- /mt/ldap.yml: -------------------------------------------------------------------------------- 1 | services: 2 | ldap: 3 | image: ${DOCKER_LDAP_IMAGE:-movabletype/test:openldap} 4 | -------------------------------------------------------------------------------- /mt/memcached.yml: -------------------------------------------------------------------------------- 1 | services: 2 | memcached: 3 | image: ${DOCKER_MEMCACHED_IMAGE:-memcached:1.5.22-alpine} 4 | -------------------------------------------------------------------------------- /mt/cgi.yml: -------------------------------------------------------------------------------- 1 | services: 2 | mt: 3 | restart: always 4 | command: "/usr/sbin/httpd -D FOREGROUND" 5 | 6 | mt-watcher: 7 | command: "/bin/true" 8 | -------------------------------------------------------------------------------- /mt/cgi-bin-mt.htaccess: -------------------------------------------------------------------------------- 1 | Options +SymLinksIfOwnerMatch 2 | 3 | RewriteEngine on 4 | RewriteRule ^mt-static - [L] 5 | RewriteRule (.*) http://mt/cgi-bin/mt/$1 [P,L] 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /.vagrant 3 | /site 4 | /mt-config.cgi 5 | /mt-config.cgi-* 6 | /.env 7 | /.env-* 8 | /.ssh-config 9 | .DS_Store 10 | /.perl-local 11 | -------------------------------------------------------------------------------- /bin/teardown-environment: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | 4 | rm -fr /tmp/mt-dev-archive-temp-* || \ 5 | echo "Failed to remove temporary directory:" /tmp/mt-dev-archive-temp-* 6 | -------------------------------------------------------------------------------- /mt/ftpd.yml: -------------------------------------------------------------------------------- 1 | services: 2 | ftpd: 3 | restart: always 4 | image: ${DOCKER_FTPD_IMAGE:-movabletype/dev:vsftpd} 5 | ports: 6 | - 21:21 7 | - 21100-21110:21100-21110 8 | -------------------------------------------------------------------------------- /mt/httpd/devcontainer/etc/mt-xdebug.ini: -------------------------------------------------------------------------------- 1 | xdebug.log=/tmp/xdebug.log 2 | xdebug.mode=develop, debug 3 | xdebug.start_with_request=yes 4 | xdebug.client_host=localhost 5 | xdebug.client_port=9003 6 | -------------------------------------------------------------------------------- /mt/mt/Dockerfile: -------------------------------------------------------------------------------- 1 | ARG DOCKER_MT_IMAGE 2 | FROM ${DOCKER_MT_IMAGE} 3 | 4 | COPY build-script/*.sh /tmp/build-script/ 5 | RUN /bin/bash /tmp/build-script/apache2.sh \ 6 | && rm -rf /tmp/build-script 7 | -------------------------------------------------------------------------------- /.devcontainer/post-create.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -x 3 | 4 | sudo apt update 5 | sudo apt install -y make git zip cpanminus perltidy libio-socket-ssl-perl 6 | sudo cpanm --force --from https://www.cpan.org App::cpm 7 | sudo apt clean 8 | -------------------------------------------------------------------------------- /mt/httpd/Dockerfile: -------------------------------------------------------------------------------- 1 | ARG DOCKER_HTTPD_IMAGE 2 | FROM ${DOCKER_HTTPD_IMAGE} 3 | 4 | COPY build-script/*.sh /tmp/build-script/ 5 | RUN for f in /tmp/build-script/*.sh; do \ 6 | /bin/bash $f; \ 7 | done \ 8 | && rm -rf /tmp/build-script 9 | -------------------------------------------------------------------------------- /mt/httpd/build-script/php.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh -x 2 | 3 | php_fpm_dirs="/etc/php-fpm.d" 4 | php_fpm_d=`ls -d $php_fpm_dirs 2>/dev/null | head -1` 5 | 6 | if [ -n "$php_fpm_d" ]; then 7 | cat > /etc/php-fpm.d/mt-env.conf <&1 | grep -v 'unzip: short read' 9 | 10 | if [ `ls | wc -l` -eq "1" ] && ls | egrep -v '^(addons|mt-static|php|alt-tmpl|plugins|default_templates|search_templates|extlib|themes|import|tmpl|tools|lib|.*\.cgi)$' >/dev/null 2>&1; then 11 | cd `ls` 12 | fi 13 | 14 | cp -R * $dest 15 | -------------------------------------------------------------------------------- /mt/mt/devcontainer/bin/invoke-perl-with-mt-libs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl 2 | 3 | use File::Find qw(find); 4 | 5 | my @libs = qw( 6 | /src/movabletype/lib 7 | /src/movabletype/extlib 8 | ); 9 | 10 | find(sub { 11 | push(@libs, $File::Find::name) 12 | if -d $File::Find::name && $_ =~ m/^(?:ext)?lib$/; 13 | }, map { glob($_) } qw(/src/*/t /src/*/plugins /src/*/addons)); 14 | 15 | $ENV{PERL5LIB} = join(':', @libs); 16 | exec '/usr/bin/perl', @ARGV; 17 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | // "image": "mcr.microsoft.com/vscode/devcontainers/base:ubuntu", 3 | // "features": { 4 | // "docker-in-docker": "latest" 5 | // }, 6 | // "remoteUser": "vscode", 7 | "postCreateCommand": ".devcontainer/post-create.sh", 8 | "customizations": { 9 | "codespaces": { 10 | "repositories": { 11 | "movabletype/*": { 12 | "permissions": "write-all" 13 | } 14 | } 15 | } 16 | }, 17 | "extensions": ["bscan.perlnavigator"] 18 | } 19 | -------------------------------------------------------------------------------- /mt/plackup-mt: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | if [ -z "$MT_UID" ]; then 4 | user=$(id -u) 5 | else 6 | user=$MT_UID 7 | fi 8 | 9 | exec starman \ 10 | -Iextlib \ 11 | -MCGI \ 12 | -MFile::Spec \ 13 | -MCGI::Cookie \ 14 | -MLWP::UserAgent \ 15 | -MHTML::Entities \ 16 | -MScalar::Util \ 17 | -MDBI \ 18 | -MDBD::mysql \ 19 | -MImage::Magick \ 20 | -E production \ 21 | --port=80 \ 22 | --workers=2 \ 23 | --pid=$MT_PID_FILE_PATH \ 24 | --user=$user \ 25 | "$@" 26 | -------------------------------------------------------------------------------- /mt/mysql.yml: -------------------------------------------------------------------------------- 1 | services: 2 | db: 3 | image: ${DOCKER_MYSQL_IMAGE:-mysql:8.0.32} 4 | environment: 5 | MYSQL_ROOT_PASSWORD: password 6 | command: ${DOCKER_MYSQL_COMMAND} ${DOCKER_MYSQL_COMMAND_AUTH_PLUGIN:-} 7 | volumes: 8 | - "..:/mt-dev" 9 | - "./mysql/conf.d:/etc/mysql/conf.d" 10 | - "${DOCKER_MYSQL_VOLUME:-mysql8}:/var/lib/mysql" 11 | volumes: 12 | mariadb10: 13 | driver: local 14 | mysql9: 15 | driver: local 16 | mysql8: 17 | driver: local 18 | mysql5: 19 | driver: local 20 | -------------------------------------------------------------------------------- /bin/git-clone: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -xe 3 | set -o pipefail 4 | 5 | repo=$1 6 | dir=$2 7 | branch=$3 8 | 9 | if [ ! -e $dir ]; then 10 | git clone $repo $dir 11 | fi 12 | 13 | if [ "$UPDATE_BRANCH" = "yes" -o "$UPDATE_BRANCH" = "1" ] && [ -n "$branch" ]; then 14 | cd $dir 15 | git fetch --all --tags --prune 16 | 17 | git checkout . 18 | git clean -f 19 | 20 | if [[ $branch == pull/* ]]; then 21 | git fetch origin refs/$branch:refs/remotes/origin/$branch 22 | fi 23 | git checkout $branch 24 | 25 | if git symbolic-ref --short HEAD 2>/dev/null; then 26 | git rebase $(git rev-parse --abbrev-ref --symbolic-full-name @{u}) 27 | fi 28 | fi 29 | -------------------------------------------------------------------------------- /mt/mt/build-script/apache2.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | conf_dirs="/etc/httpd/conf.d /etc/apache2/conf-enabled /etc/httpd/conf/extra" 4 | module_dirs="/usr/lib/apache2/modules /usr/lib64/httpd/modules /usr/lib/httpd/modules" 5 | 6 | rm -f /etc/apache2/sites-enabled/default-ssl.conf # disable ssl 7 | 8 | mkdir -m 777 -p /tmp/apache2/log /tmp/apache2/run 9 | 10 | httpd_conf_d=`ls -d $conf_dirs 2>/dev/null | head -1` 11 | cat > $httpd_conf_d/mt.conf </dev/null | head -1` 16 | if [ -n "$mod_env_so" ]; then 17 | cat > $httpd_conf_d/mt-env.conf </dev/null 2>&1; then 6 | md5=md5sum 7 | else 8 | md5=md5 9 | fi 10 | 11 | script_dir=$(cd $(dirname $0); pwd) 12 | base=$1; shift 13 | dest=$1; shift 14 | 15 | script=extract-archive-on-docker 16 | 17 | tmpdir=/tmp/mt-dev-extract-archive-$$ 18 | mkdir $tmpdir 19 | for f in "$@"; do 20 | if echo "$f" | egrep -q "^https?://"; then 21 | name=$(basename $f) 22 | path=$tmpdir/$name 23 | curl -s -L -o $path $f 24 | else 25 | name=$f 26 | path=$base/$f 27 | fi 28 | $md5 $path 29 | docker run --rm -v $dest:/dest -v $path:/archive/$name -v $script_dir/$script:/usr/local/bin/$script -w /dest busybox:uclibc /usr/local/bin/$script /dest /archive/$name 30 | done 31 | rm -fr $tmpdir 32 | 33 | docker run --rm -v $dest:/dest busybox:uclibc ls /dest/mt.cgi > /dev/null 34 | -------------------------------------------------------------------------------- /ssl/generate-certs.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | mkdir -p certs 4 | touch index.txt 5 | echo 1000 > serial 6 | 7 | openssl genrsa -out certs/ca-key.pem 2048 8 | openssl req -new -x509 -nodes -days 3650 -key certs/ca-key.pem -out certs/ca-cert.pem \ 9 | -subj "/C=JP/ST=Tokyo/L=Tokyo/O=MyOrganization/CN=MyCA" 10 | 11 | openssl genrsa -out certs/server-key.pem 2048 12 | openssl req -new -key certs/server-key.pem -out certs/server-req.pem \ 13 | -subj "/C=JP/ST=Tokyo/L=Tokyo/O=MyOrganization/CN=db" 14 | openssl x509 -req -in certs/server-req.pem -days 3650 \ 15 | -CA certs/ca-cert.pem -CAkey certs/ca-key.pem -CAcreateserial \ 16 | -out certs/server-cert.pem 17 | 18 | openssl genrsa -out certs/client-key.pem 2048 19 | openssl req -new -key certs/client-key.pem -out certs/client-req.pem \ 20 | -subj "/C=JP/ST=Tokyo/L=Tokyo/O=MyOrganization/CN=mysqlclient" 21 | openssl x509 -req -in certs/client-req.pem -days 3650 \ 22 | -CA certs/ca-cert.pem -CAkey certs/ca-key.pem -CAcreateserial \ 23 | -out certs/client-cert.pem 24 | 25 | chmod 644 certs/* 26 | -------------------------------------------------------------------------------- /mt/httpd/Dockerfile.devcontainer: -------------------------------------------------------------------------------- 1 | ARG DOCKER_HTTPD_IMAGE 2 | 3 | FROM ${DOCKER_HTTPD_IMAGE} 4 | 5 | # [Option] Install zsh 6 | ARG INSTALL_ZSH="true" 7 | # [Option] Upgrade OS packages to their latest versions 8 | ARG UPGRADE_PACKAGES="false" 9 | 10 | # Install needed packages and setup non-root user. Use a separate RUN statement to add your own dependencies. 11 | ARG USERNAME=vscode 12 | ARG USER_UID=1000 13 | ARG USER_GID=$USER_UID 14 | COPY devcontainer/build-script/*.sh /tmp/build-script/ 15 | RUN apt-get update \ 16 | && /bin/bash /tmp/build-script/common-debian.sh "${INSTALL_ZSH}" "${USERNAME}" "${USER_UID}" "${USER_GID}" "${UPGRADE_PACKAGES}" "true" "true" \ 17 | && apt-get autoremove -y && apt-get clean -y && rm -rf /var/lib/apt/lists/* /tmp/build-script 18 | 19 | COPY build-script/*.sh /tmp/build-script/ 20 | RUN /bin/bash /tmp/build-script/apache2.sh \ 21 | && rm -rf /tmp/build-script 22 | 23 | RUN set -ex && \ 24 | docker-php-ext-install mysqli && \ 25 | pecl install xdebug && \ 26 | docker-php-ext-enable xdebug 27 | COPY devcontainer/etc/mt-xdebug.ini /usr/local/etc/php/conf.d/mt-xdebug.ini 28 | 29 | USER vscode 30 | -------------------------------------------------------------------------------- /mt/docker-entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | [ "$(id -u)" -eq 0 ] && SUDO= || SUDO=sudo 4 | 5 | if [ -n "$DOCKER_MT_SERVICES" ]; then 6 | for s in $DOCKER_MT_SERVICES; do 7 | $SUDO service $s start 8 | done 9 | fi 10 | 11 | if [ -n "$DOCKER_MT_CPANFILES" ]; then 12 | for f in $DOCKER_MT_CPANFILES; do 13 | if [ -f $f ]; then 14 | $SUDO cpm install -g --cpanfile=$f 15 | fi 16 | done 17 | fi 18 | 19 | $SUDO chmod 777 /var/www/html 20 | $SUDO chmod 777 /var/www/cgi-bin/mt/mt-static/support 21 | $SUDO chmod 777 /var/www/cgi-bin/mt/themes 22 | 23 | if [ "$1" = "apache2-foreground" ]; then 24 | # invoke php-fpm 25 | if [ -e /usr/sbin/php-fpm ]; then 26 | mkdir -p /run/php-fpm 27 | /usr/sbin/php-fpm 28 | fi 29 | 30 | if [ -e /usr/local/bin/apache2-foreground ]; then 31 | exec /usr/local/bin/apache2-foreground 32 | else 33 | exec /usr/sbin/httpd -D FOREGROUND 34 | fi 35 | else 36 | if [ "$MT_DEV_UPDATE_BRANCH" = "yes" -o "$MT_DEV_UPDATE_BRANCH" = "1" ] && [ -e "/var/www/cgi-bin/mt/Makefile" ]; then 37 | make -C /var/www/cgi-bin/mt clean me 38 | fi 39 | 40 | exec "$@" 41 | fi 42 | -------------------------------------------------------------------------------- /mt/mt-watcher.pl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env perl 2 | 3 | use strict; 4 | use warnings; 5 | 6 | use Cwd qw(getcwd); 7 | use Filesys::Notify::Simple; 8 | 9 | if ( $ENV{DISABLE_MT_WATCHER} ) { 10 | warn('mt-watcher container is disbled.'); 11 | exit; 12 | } 13 | 14 | # wait for `make me` to complete 15 | sleep(5); 16 | 17 | my $mt_home = $ENV{MT_HOME} || getcwd(); 18 | $mt_home =~ s{/+$}{}; # remove trailing slash 19 | 20 | my @files = ( map( {"$mt_home/$_"} qw(addons extlib lib plugins) ), glob("$mt_home/*.cgi") ); 21 | my $watcher = Filesys::Notify::Simple->new( \@files ); 22 | while (1) { 23 | $watcher->wait( 24 | sub { 25 | my @paths = grep { 26 | 27 | # exclude files other than "*.cgi" at the top level 28 | $_ =~ m{$mt_home/(?:[^/]+\.cgi|.*?/)}; 29 | } map { $_->{path} } @_; 30 | 31 | return unless @paths; 32 | 33 | print( join( "\n", @paths ) . "\n" ); 34 | my $mt_container_id = `docker ps -q --filter label=mt-dev.service=mt`; 35 | system("docker kill -s HUP $mt_container_id"); 36 | 37 | # throttling 38 | sleep(1); 39 | } 40 | ); 41 | } 42 | -------------------------------------------------------------------------------- /mt/mt/Dockerfile.devcontainer: -------------------------------------------------------------------------------- 1 | ARG DOCKER_NODEJS_IMAGE 2 | ARG DOCKER_MT_IMAGE 3 | 4 | FROM ${DOCKER_NODEJS_IMAGE} as node 5 | FROM ${DOCKER_MT_IMAGE} 6 | 7 | # [Option] Install zsh 8 | ARG INSTALL_ZSH="true" 9 | # [Option] Upgrade OS packages to their latest versions 10 | ARG UPGRADE_PACKAGES="false" 11 | 12 | # Install needed packages and setup non-root user. Use a separate RUN statement to add your own dependencies. 13 | ARG USERNAME=vscode 14 | ARG USER_UID=1000 15 | ARG USER_GID=$USER_UID 16 | COPY devcontainer/build-script/*.sh /tmp/build-script/ 17 | RUN apt-get update \ 18 | && /bin/bash /tmp/build-script/common-debian.sh "${INSTALL_ZSH}" "${USERNAME}" "${USER_UID}" "${USER_GID}" "${UPGRADE_PACKAGES}" "true" "true" \ 19 | && apt-get autoremove -y && apt-get clean -y && rm -rf /var/lib/apt/lists/* /tmp/build-script 20 | 21 | COPY build-script/*.sh /tmp/build-script/ 22 | RUN /bin/bash /tmp/build-script/apache2.sh \ 23 | && rm -rf /tmp/build-script 24 | 25 | RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ 26 | && apt-get -y install --no-install-recommends \ 27 | g++ libio-aio-perl \ 28 | && apt-get autoremove -y && apt-get clean -y && rm -rf /var/lib/apt/lists/* 29 | RUN cpm install -g \ 30 | PLS Perl::LanguageServer 31 | 32 | COPY devcontainer/bin/* /usr/local/bin/ 33 | 34 | COPY --from=node /usr/local/bin/ /usr/local/bin/ 35 | COPY --from=node /usr/local/lib/node_modules/ /usr/local/lib/node_modules/ 36 | COPY --from=node /opt/ /opt/ 37 | 38 | USER vscode 39 | -------------------------------------------------------------------------------- /ssl/openssl.cnf: -------------------------------------------------------------------------------- 1 | [ ca ] 2 | default_ca = CA_default 3 | 4 | [ CA_default ] 5 | dir = . 6 | certs = $dir/certs 7 | crl_dir = $dir/crl 8 | new_certs_dir = $dir/newcerts 9 | database = $dir/index.txt 10 | serial = $dir/serial 11 | RANDFILE = $dir/private/.rand 12 | 13 | private_key = $dir/private/ca-key.pem 14 | certificate = $dir/certs/ca-cert.pem 15 | 16 | default_days = 3650 17 | default_crl_days = 30 18 | default_md = sha256 19 | preserve = no 20 | 21 | policy = policy_match 22 | 23 | [ policy_match ] 24 | countryName = optional 25 | stateOrProvinceName = optional 26 | organizationName = optional 27 | organizationalUnitName = optional 28 | commonName = supplied 29 | emailAddress = optional 30 | 31 | [ req ] 32 | default_bits = 2048 33 | default_md = sha256 34 | default_keyfile = privkey.pem 35 | distinguished_name = req_distinguished_name 36 | x509_extensions = v3_ca 37 | req_extensions = v3_req 38 | 39 | [ req_distinguished_name ] 40 | countryName = Country Name (2 letter code) 41 | countryName_default = JP 42 | stateOrProvinceName = State or Province Name (full name) 43 | stateOrProvinceName_default = Tokyo 44 | localityName = Locality Name (eg, city) 45 | localityName_default = Tokyo 46 | organizationName = Organization Name (eg, company) 47 | organizationName_default = MyOrganization 48 | commonName = Common Name (eg, YOUR name) 49 | commonName_max = 64 50 | 51 | [ v3_ca ] 52 | subjectKeyIdentifier=hash 53 | authorityKeyIdentifier=keyid:always,issuer 54 | basicConstraints = critical,CA:true 55 | 56 | [ v3_req ] 57 | basicConstraints = CA:FALSE 58 | keyUsage = nonRepudiation, digitalSignature, keyEncipherment 59 | extendedKeyUsage = serverAuth, clientAuth 60 | -------------------------------------------------------------------------------- /mt-config.cgi-original: -------------------------------------------------------------------------------- 1 | ## Movable Type configuration file ## 2 | ## ## 3 | ## This file defines system-wide settings for Movable Type ## 4 | ## In total, there are over a hundred options, but only those ## 5 | ## critical for everyone are listed below. ## 6 | ## ## 7 | ## Information on all others can be found at: ## 8 | ## http://www.movabletype.org/documentation/appendices/config-directives/ ## 9 | 10 | ################################################################ 11 | ##################### REQUIRED SETTINGS ######################## 12 | ################################################################ 13 | 14 | # The CGIPath is the URL to your Movable Type directory 15 | CGIPath /cgi-bin/mt/ 16 | 17 | # The StaticWebPath is the URL to your mt-static directory 18 | # Note: Check the installation documentation to find out 19 | # whether this is required for your environment. If it is not, 20 | # simply remove it or comment out the line by prepending a "#". 21 | StaticWebPath /mt-static 22 | 23 | #================ DATABASE SETTINGS ================== 24 | # CHANGE setting below that refer to databases 25 | # you will be using. 26 | 27 | ##### MYSQL ##### 28 | ObjectDriver DBI::mysql 29 | Database mt 30 | DBUser root 31 | DBPassword password 32 | DBHost db 33 | 34 | ## Change setting to language that you want to using. 35 | #DefaultLanguage en_US 36 | #DefaultLanguage de 37 | #DefaultLanguage es 38 | #DefaultLanguage fr 39 | DefaultLanguage ja 40 | #DefaultLanguage nl 41 | 42 | BaseSitePath /var/www/html 43 | PIDFilePath /tmp/mt.psgi.pid 44 | 45 | MemcachedServers memcached:11211 46 | 47 | DBIConnectOptions mysql_ssl=1 48 | DBIConnectOptions mysql_ssl_verify_server_cert=1 49 | DBIConnectOptions mysql_ssl_client_key=/mt-dev/ssl/certs/client-key.pem 50 | DBIConnectOptions mysql_ssl_client_cert=/mt-dev/ssl/certs/client-cert.pem 51 | DBIConnectOptions mysql_ssl_ca_file=/mt-dev/ssl/certs/ca-cert.pem 52 | -------------------------------------------------------------------------------- /mt/httpd/build-script/apache2.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh -x 2 | 3 | conf_dirs="/etc/httpd/conf.d /etc/apache2/conf-enabled /etc/httpd/conf/extra" 4 | module_dirs="/usr/lib/apache2/modules /usr/lib64/httpd/modules /usr/lib/httpd/modules" 5 | 6 | rm -f /etc/apache2/sites-enabled/default-ssl.conf # disable ssl 7 | 8 | mkdir -m 777 -p /tmp/apache2/log /tmp/apache2/run 9 | 10 | httpd_conf_d=`ls -d $conf_dirs 2>/dev/null | head -1` 11 | cat > $httpd_conf_d/mt.conf </dev/null | head -1` 22 | if [ -n "$mod_rewrite_so" ]; then 23 | cat > $httpd_conf_d/mt-rewrite.conf </dev/null | head -1` 29 | if [ -n "$mod_proxy_so" ]; then 30 | cat > $httpd_conf_d/mt-proxy.conf </dev/null | head -1` 37 | if [ -n "$mod_proxy_http_so" ]; then 38 | cat > $httpd_conf_d/mt-proxy_http.conf </dev/null | head -1` 45 | if [ -n "$mod_include_so" ]; then 46 | cat > $httpd_conf_d/mt-include.conf < 49 | 50 | Options +Includes 51 | AddOutputFilter INCLUDES .html 52 | 53 | 54 | CONF 55 | fi 56 | 57 | if [ $httpd_conf_d = "/etc/httpd/conf/extra" ]; then 58 | # archlinux 59 | cat >> /etc/httpd/conf/httpd.conf </dev/null | head -1` 65 | if [ -n "$mod_env_so" ]; then 66 | cat > $httpd_conf_d/mt-env.conf <" 28 | return 1 29 | end 30 | 31 | commands = [] 32 | 33 | if @argv[0] == "check-ssh-key" 34 | with_target_vms(nil, single_target: true) do |vm| 35 | if vm.state.id != :running 36 | env = vm.action(:up) 37 | if vm.state.id != :running 38 | return error(1) 39 | end 40 | end 41 | 42 | env = vm.action(:ssh_run, ssh_run_command: 'ssh git@github.com 2>&1 | grep "successfully authenticated" > /dev/null', ssh_opts: { extra_args: %W(-q -t) }) 43 | 44 | status = env[:ssh_run_exit_status] || 0 45 | 46 | if status != 0 47 | puts <<~MSG 48 | Please execute this command first for copy your private key for github. 49 | 50 | $ vagrant mt-dev copy-ssh-key ~/.ssh/id_rsa 51 | 52 | ~/.ssh/id_rsa is a typical filename for your private key, but it's may be different in your environment. 53 | 54 | See also: 55 | https://github.com/movabletype/mt-dev/wiki/Troubleshooting 56 | MSG 57 | return 1 58 | end 59 | end 60 | 61 | print "\e[32m" 62 | puts "Succeeded!" 63 | print "\e[0m" 64 | 65 | return 0 66 | elsif @argv[0] == "copy-ssh-key" 67 | file = @argv.delete_at(1) 68 | key = begin 69 | File.read(file) 70 | rescue => e 71 | puts e 72 | return error(1) 73 | end 74 | 75 | commands += [ 76 | "mkdir -p /home/vagrant/.ssh", 77 | "chmod 700 /home/vagrant/.ssh", 78 | "chown vagrant:vagrant /home/vagrant/.ssh", 79 | "echo -n #{Shellwords.shellescape(key)} > /home/vagrant/.ssh/id_rsa", 80 | "chmod 600 /home/vagrant/.ssh/id_rsa", 81 | "chown vagrant:vagrant /home/vagrant/.ssh/id_rsa", 82 | "ssh-keygen -l -f /home/vagrant/.ssh/id_rsa > /tmp/id_rsa_info", 83 | "perl -e '($len) = split(/ /, <>); if ($len <= 1024) { print qq{Invalid key length.\\n}; unlink(q{/home/vagrant/.ssh/id_rsa}); exit(1)}' < /tmp/id_rsa_info", 84 | ] 85 | elsif @argv[0] == "remove-ssh-key" 86 | commands += [ 87 | "rm /home/vagrant/.ssh/id_rsa", 88 | ] 89 | elsif @argv[0] == "publish-ssh-config" 90 | config = with_captured_stdout do 91 | Vagrant.plugin("2").manager.commands["ssh-config".to_sym][0].call.new(%W(--host mt-dev), @env).execute 92 | end 93 | 94 | config_file = File.expand_path('.', '.ssh-config') 95 | fh = File.open(config_file, 'w') 96 | fh.puts config 97 | 98 | puts "You can connect to mt-dev by using this config file.\n" 99 | puts 100 | puts config_file 101 | 102 | return 103 | else 104 | argv = @argv.map { |str| Shellwords.shellescape(str) }.join(" ") 105 | commands.push("cd /home/vagrant/mt-dev && make HTTPD_HOST_NAME=#{$mt_dev_vm_private_network_ip} " + argv) 106 | end 107 | 108 | with_target_vms(nil, single_target: true) do |vm| 109 | if vm.state.id != :running 110 | env = vm.action(:up) 111 | if vm.state.id != :running 112 | return error(1) 113 | end 114 | end 115 | 116 | env = vm.action(:ssh_run, ssh_run_command: commands.join(" && "), ssh_opts: { extra_args: %W(-q -t) }) 117 | 118 | status = env[:ssh_run_exit_status] || 0 119 | 120 | if status == 0 121 | print "\e[32m" 122 | puts "Succeeded!" 123 | print "\e[0m" 124 | 125 | return status 126 | end 127 | 128 | return error(1) 129 | end 130 | end 131 | end 132 | 133 | class MtDev < Vagrant.plugin("2") 134 | name "mt-dev" 135 | 136 | command "mt-dev" do 137 | MtDevCommand 138 | end 139 | end 140 | 141 | Vagrant.configure("2") do |config| 142 | config.vm.box = "bento/ubuntu-24.04" 143 | config.vm.network "private_network", ip: $mt_dev_vm_private_network_ip 144 | config.vm.hostname = "mt-dev" 145 | 146 | config.vm.synced_folder ".", "/vagrant", disabled: true 147 | if RUBY_PLATFORM =~ /darwin/ 148 | config.vm.synced_folder ".", 149 | "/home/vagrant/mt-dev", 150 | type: "nfs", 151 | nfs_udp: false, 152 | mount_options: ["noatime", "nolock"] 153 | else 154 | config.vm.synced_folder ".", "/home/vagrant/mt-dev" 155 | end 156 | 157 | config.ssh.forward_agent = true 158 | 159 | config.vm.provision "shell", inline: <<-SHELL 160 | export DEBIAN_FRONTEND=noninteractive 161 | apt-get update 162 | apt-get upgrade -y # apply security update 163 | apt-get install -y make git zip \ 164 | docker.io docker-compose-v2 docker-buildx \ 165 | mysql-client 166 | # required by HTTP::Tiny 167 | apt-get install -y libio-socket-ssl-perl 168 | adduser vagrant docker 169 | SHELL 170 | 171 | config.vm.provider "virtualbox" do |vb, override| 172 | vb.memory = ENV["VM_VB_MEMORY"] || 2048 173 | vb.customize ["modifyvm", :id, "--uartmode1", "disconnected"] 174 | 175 | override.vm.network :forwarded_port, 176 | guest: ENV["HTTPD_EXPOSE_PORT"] || 80, 177 | host: ENV["VM_VB_HTTP_PORT"] || ENV["HTTPD_EXPOSE_PORT"] || 5825 178 | end 179 | end 180 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | MAKEFILE_DIR=$(abspath $(dir $(lastword $(MAKEFILE_LIST)))) 2 | 3 | export BASE_SITE_PATH:=${MAKEFILE_DIR}/site 4 | export DOCKER:=docker 5 | export DOCKER_COMPOSE:=${shell ${DOCKER} compose >/dev/null 2>&1 && echo 'docker compose' || echo 'docker-compose'} 6 | export DOCKER_COMPOSE_YAML_MIDDLEWARES:=-f ./mt/mysql.yml -f ./mt/memcached.yml 7 | export UP_ARGS:=-d 8 | export MT_HOME_PATH:=${MAKEFILE_DIR}/../movabletype 9 | export HTTPD_HOST_NAME:=localhost 10 | export HTTPD_EXPOSE_PORT:=80 11 | export UPDATE_BRANCH:=yes 12 | export UPDATE_DOCKER_IMAGE:=yes 13 | export CREATE_DATABASE_IF_NOT_EXISTS:=yes 14 | export DOCKER_MT_CPANFILES:=t/cpanfile 15 | 16 | MT_CONFIG_CGI=${shell [ -e mt-config.cgi ] && echo mt-config.cgi || echo mt-config.cgi-original} 17 | BASE_ARCHIVE_PATH=${MAKEFILE_DIR}/archive 18 | 19 | # shortcuts. 20 | ifneq (${PHP},) 21 | DOCKER_HTTPD_IMAGE=movabletype/test:php-${PHP} 22 | endif 23 | ifneq (${PERL},) 24 | DOCKER_MT_IMAGE=movabletype/test:perl-${PERL} 25 | endif 26 | ifneq (${NODE},) 27 | DOCKER_NODEJS_IMAGE=node:${NODE} 28 | endif 29 | ifneq (${DB},) 30 | DOCKER_MYSQL_IMAGE=${DB} 31 | endif 32 | ifneq (${MT_EXPOSE_PORT},) 33 | export DOCKER_COMPOSE_YAML_EXPOSE=-f ./mt/mt-expose.yml 34 | endif 35 | 36 | export DOCKER_COMPOSE_USER_YAML 37 | export DOCKER_MT_BUILD_CONTEXT 38 | export DOCKER_MT_DOCKERFILE 39 | export DOCKER_MT_IMAGE 40 | export DOCKER_MT_SERVICES 41 | export DOCKER_MT_CPANFILES 42 | export DOCKER_NODEJS_IMAGE 43 | export DOCKER_HTTPD_BUILD_CONTEXT 44 | export DOCKER_HTTPD_DOCKERFILE 45 | export DOCKER_HTTPD_IMAGE 46 | export DOCKER_MYSQL_IMAGE 47 | export DOCKER_MYSQL_COMMAND 48 | export DOCKER_MEMCACHED_IMAGE 49 | export DOCKER_LDAP_IMAGE 50 | export DOCKER_FTPD_IMAGE 51 | export DOCKER_MAILPIT_IMAGE 52 | export DOCKER_VOLUME_MOUNT_FLAG 53 | export MT_RUN_VIA 54 | export MT_EXPOSE_PORT 55 | export MT_UID 56 | export MAILPIT_EXPOSE_PORT 57 | export PLACKUP 58 | export CMD 59 | 60 | # mt-watcher container 61 | export DISABLE_MT_WATCHER 62 | export PERL_FNS_NO_OPT 63 | 64 | 65 | # override variables 66 | ENV_FILE=.env 67 | -include ${MAKEFILE_DIR}/${ENV_FILE} 68 | 69 | 70 | # setup internal variables 71 | 72 | MT_CONFIG_CGI_SRC_PATH=${shell perl -e 'print("${MT_CONFIG_CGI}" =~ m{/} ? "${MT_CONFIG_CGI}" : "${MAKEFILE_DIR}/${MT_CONFIG_CGI}")' } 73 | export MT_CONFIG_CGI_SRC_PATH 74 | 75 | ifneq (${WITHOUT_MT_CONFIG_CGI},) 76 | export MT_CONFIG_CGI_DEST_PATH=/tmp/mt-config.cgi 77 | endif 78 | 79 | ifeq ($(wildcard ${MT_CONFIG_CGI_SRC_PATH}),) 80 | $(error You should create ${MT_CONFIG_CGI_SRC_PATH} first.) 81 | endif 82 | 83 | _DC=${DOCKER_COMPOSE} -f ./mt/common.yml ${DOCKER_COMPOSE_YAML_MIDDLEWARES} ${_DC_YAML_OVERRIDE} ${DOCKER_COMPOSE_YAML_EXPOSE} ${DOCKER_COMPOSE_USER_YAML} 84 | _DATABASE=${shell perl -ne 'print $$1 if /^Database\s+([\w-]+)/' < ${MT_CONFIG_CGI_SRC_PATH}} 85 | 86 | .PHONY: db up down 87 | 88 | up: up-cgi 89 | 90 | fixup: 91 | @perl -e 'exit($$ENV{MT_HOME_PATH} =~ m{/})' || \ 92 | for f in mt-config.cgi mt-tb.cgi mt-comment.cgi .htaccess; do \ 93 | fp=${MT_HOME_PATH}/$$f; \ 94 | [ -d $$fp ] && rmdir $$fp || true; \ 95 | [ -f $$fp ] && perl -e 'exit((stat(shift))[7] == 0 ? 0 : 1)' $$fp && rm -f $$fp || true; \ 96 | done 97 | @chmod -R go-w mt/mysql/conf.d 98 | 99 | setup-mysql-volume: 100 | $(eval export DOCKER_MYSQL_VOLUME=$(shell echo ${DOCKER_MYSQL_IMAGE} | sed -e 's/\..*//; s/[^a-zA-Z0-9]//g')) 101 | $(eval export DOCKER_MYSQL_COMMAND_AUTH_PLUGIN=$(shell if ! echo ${DOCKER_MYSQL_IMAGE} | egrep -q '^mysql:(9|[1-9][0-9]+)$$'; then echo '--default-authentication-plugin=mysql_native_password'; fi)) 102 | 103 | ifneq (${SQL},) 104 | MYSQL_COMMAND_ARGS=-e '${SQL}' 105 | endif 106 | 107 | update-ssl: 108 | ${DOCKER} run --rm -v ${MAKEFILE_DIR}/ssl:/ssl -w /ssl --entrypoint /bin/sh alpine/openssl:latest generate-certs.sh 109 | 110 | exec-mysql: 111 | opt=""; if ! [ -t 0 ] ; then opt="-T" ; fi; \ 112 | ${_DC} exec $$opt db mysql -uroot -ppassword -hlocalhost ${MYSQL_COMMAND_ARGS} 113 | 114 | # FIXME: 115 | exec-ldappasswd: 116 | ${_DC} exec ldap ldappasswd -x -D "cn=admin,dc=example,dc=com" -w secret "cn=Melody,ou=users,dc=example,dc=com" -S 117 | 118 | 119 | up-cgi: MT_RUN_VIA=cgi 120 | up-cgi: up-common 121 | 122 | up-psgi: MT_RUN_VIA=psgi 123 | up-psgi: up-common 124 | 125 | 126 | ifeq (${RECIPE},) 127 | ARCHIVE_FOR_SETUP="" 128 | else 129 | ARCHIVE_FOR_SETUP=${ARCHIVE} 130 | endif 131 | 132 | up-common: down fixup update-ssl 133 | ${MAKE} down-mt-home-volume 134 | ${DOCKER} volume create --label mt-dev-mt-home-tmp mt-dev-mt-home-tmp 135 | 136 | ifneq (${ARCHIVE},) 137 | ifeq (${RECIPE},) 138 | # TBD: random name? 139 | $(eval MT_HOME_PATH=mt-dev-mt-home-tmp) 140 | ${MAKEFILE_DIR}/bin/extract-archive ${BASE_ARCHIVE_PATH} ${MT_HOME_PATH} $(shell echo ${ARCHIVE} | tr ',' ' ') 141 | endif 142 | endif 143 | 144 | $(eval export _ARGS=$(shell UPDATE_BRANCH=${UPDATE_BRANCH} ${MAKEFILE_DIR}/bin/setup-environment --recipe "$(shell echo ${RECIPE} | tr ',' ' ')" --repo "$(shell echo ${REPO} | tr ',' ' ')" --pr "$(shell echo ${PR} | tr ',' ' ')" --archive "$(shell echo ${ARCHIVE_FOR_SETUP} | tr ',' ' ')")) 145 | ifneq (${RECIPE},) 146 | @perl -e 'exit(length($$ENV{_ARGS}) > 0 ? 0 : 1)' 147 | endif 148 | ifneq (${REPO},) 149 | @perl -e 'exit(length($$ENV{_ARGS}) > 0 ? 0 : 1)' 150 | endif 151 | ifneq (${PR},) 152 | @perl -e 'exit(length($$ENV{_ARGS}) > 0 ? 0 : 1)' 153 | endif 154 | ${MAKE} up-common-invoke-docker-compose MT_HOME_PATH=${MT_HOME_PATH} ${_ARGS} RECIPE="" REPO="" PR="" $(shell [ -n "${DOCKER_MT_IMAGE}" ] && echo "DOCKER_MT_IMAGE=${DOCKER_MT_IMAGE}") $(shell [ -n "${DOCKER_MYSQL_IMAGE}" ] && echo "DOCKER_MYSQL_IMAGE=${DOCKER_MYSQL_IMAGE}") 155 | @if [ -t 1 ] && which tput >/dev/null 2>&1 && [ $$(tput colors 2>/dev/null || echo 0) -ge 8 ]; then \ 156 | echo "\n\033[32m➜\033[0m Movable Type is running on http://${HTTPD_HOST_NAME}:${HTTPD_EXPOSE_PORT}/cgi-bin/mt/mt.cgi"; \ 157 | else \ 158 | echo "\n➜ Movable Type is running on http://${HTTPD_HOST_NAME}:${HTTPD_EXPOSE_PORT}/cgi-bin/mt/mt.cgi"; \ 159 | fi 160 | 161 | up-common-invoke-docker-compose: _DC_YAML_OVERRIDE=-f ./mt/${MT_RUN_VIA}.yml ${DOCKER_COMPOSE_YAML_OVERRIDE} 162 | up-common-invoke-docker-compose: setup-mysql-volume 163 | @echo MT_HOME_PATH=${MT_HOME_PATH} 164 | @echo BASE_SITE_PATH=${BASE_SITE_PATH} 165 | @echo DOCKER_MT_IMAGE=${DOCKER_MT_IMAGE} 166 | @echo DOCKER_HTTPD_IMAGE=${DOCKER_HTTPD_IMAGE} 167 | @echo DOCKER_MYSQL_IMAGE=${DOCKER_MYSQL_IMAGE} 168 | ifeq (${UPDATE_DOCKER_IMAGE},yes) 169 | ${_DC} pull 170 | ${_DC} build --pull 171 | endif 172 | ifeq (${CREATE_DATABASE_IF_NOT_EXISTS},yes) 173 | ifneq (${_DATABASE},) 174 | ${_DC} up -d db 175 | @while ! ${MAKE} exec-mysql MYSQL_COMMAND_ARGS="-e 'SELECT 1'" >/dev/null 2>&1; do \ 176 | sleep 1; \ 177 | done 178 | ${MAKE} exec-mysql MYSQL_COMMAND_ARGS="-e 'CREATE DATABASE IF NOT EXISTS \`${_DATABASE}\` /* DEFAULT CHARACTER SET utf8mb4 */;'" 179 | endif 180 | endif 181 | ${_DC} up ${UP_ARGS} 182 | 183 | 184 | ifneq (${REMOVE_VOLUME},) 185 | DOWN_ARGS=-v 186 | endif 187 | 188 | down: 189 | ${_DC} down --remove-orphans ${DOWN_ARGS} 190 | ${MAKEFILE_DIR}/bin/teardown-environment 191 | ${MAKE} down-mt-home-volume 192 | 193 | down-mt-home-volume: 194 | @for v in `docker volume ls -f label=mt-dev-mt-home-tmp | sed -e '1d' | awk '{print $$2}'`; do \ 195 | docker volume rm $$v; \ 196 | done 197 | 198 | 199 | clean-config: 200 | rm ~/.mt-dev.conf 201 | 202 | clean-image: down 203 | ${DOCKER} images | grep movabletype | awk '{ print $$3 }' | xargs ${DOCKER} rmi -f 204 | 205 | docker-compose: 206 | ${_DC} ${ARGS} 207 | 208 | 209 | # aliases 210 | 211 | logs: ARGS=logs 212 | logs: docker-compose 213 | 214 | ifeq (${CMD},) 215 | mt-shell: ARGS=exec mt /bin/bash 216 | mt-shell: docker-compose 217 | else 218 | mt-shell: 219 | ${_DC} exec -e CMD mt /bin/bash -c "$$CMD" 220 | endif 221 | 222 | cpan-install: 223 | ${_DC} exec mt cpm install -g ${ARGS} 224 | ${_DC} exec mt kill 1 225 | 226 | cpan-uninstall: 227 | ${_DC} exec mt bash -c "cpm install -g App::cpanminus && cpanm -fU ${ARGS}" 228 | ${_DC} exec mt kill 1 229 | 230 | cp-R: 231 | mkdir -p ${TO} 232 | ${_DC} exec -T mt tar -C ${FROM} -zcf - . | tar -C ${TO} -zxf - 233 | 234 | 235 | build: 236 | ${MAKE} -C docker 237 | 238 | CODE_CPANM=$(shell type cpm >/dev/null 2>&1 && echo "cpm install" || echo "cpanm --installdeps .") 239 | CODE_WORKSPACES_DIR=$(abspath ${MAKEFILE_DIR}/..) 240 | CODE_DIR=${MAKEFILE_DIR}/.code 241 | CODE_CODE_WORKSPACE_FILE=${CODE_DIR}/mt.code-workspace 242 | 243 | code-init: 244 | mkdir -p ${CODE_DIR} 245 | 246 | LIBS=$(shell \ 247 | find .. -maxdepth 4 -type d \ 248 | \( -name 'lib' -o -name 'extlib' \) \ 249 | -not -path '*/node_modules/*' \ 250 | -not -path '*/bower_components/*' \ 251 | -not -path '*/local/*' \ 252 | -not -path '*/.*/*' \ 253 | | sed -e 's/^..\///' | grep -v 'movabletype-patches') 254 | code-generate-workspace: code-init 255 | echo '{"folders":[' > ${CODE_CODE_WORKSPACE_FILE} 256 | for d in `echo ${LIBS} | perl -pe 's/\/\S+//g; s/ /\\n/g' | sort -u`; do \ 257 | printf '%s%s%s' '{"path":"' ${CODE_WORKSPACES_DIR}/$$d '"},' >> ${CODE_CODE_WORKSPACE_FILE}; \ 258 | done 259 | echo '],"settings":{"perlnavigator.includePaths":["${CODE_DIR}/local/lib/perl5",' >> ${CODE_CODE_WORKSPACE_FILE} 260 | for d in ${LIBS}; do \ 261 | printf '%s%s%s' '"' ${CODE_WORKSPACES_DIR}/$$d '",' >> ${CODE_CODE_WORKSPACE_FILE}; \ 262 | done 263 | echo ']}' >> ${CODE_CODE_WORKSPACE_FILE} 264 | 265 | code-cpanm-install: code-init 266 | cd `ls -d ../*movabletype/t | head -n 1` && ${CODE_CPANM} -L${CODE_DIR}/local || true 267 | 268 | code-open-workspace: code-cpanm-install code-generate-workspace 269 | code ${CODE_CODE_WORKSPACE_FILE} 270 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 5 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 6 | 7 | ## [Unreleased] 8 | 9 | ### Added 10 | 11 | * If the given URL is http://github.com/... and returns 404, then try to download the archive with the `gh` command and try to download the archive at http://github.com/... 12 | * Allow starman's --user option to be specified in the MT_UID environment variable. 13 | 14 | ## [2.8.0] - 2025-04-23 15 | 16 | ### Changed 17 | 18 | * Make the generated certificate readable by any MySQL server. 19 | * Print the URL of the running server. 20 | * Use "/usr/bin/env bash" instead of "/bin/bash" in the shell script. 21 | * Execute post-create.sh as much as possible for codespaces 22 | * Stop setup-environment script when pr option is not used 23 | * Persistent mailpit database. 24 | 25 | ### Fixed 26 | 27 | * Fix permission issue of mysql conf.d directory. 28 | * Remove write permission because it is an error in environments where write permission is added to other by default. 29 | * Fix the path mapping of plugins. 30 | * Fixed a problem in which the path of a plugin is misaligned if it contains regular expression meta characters such as "+". 31 | 32 | ## [2.7.0] - 2024-11-15 33 | 34 | ### Added 35 | 36 | * Added support for mysql:9.x docker images. 37 | * Enable to use SSL connection to MySQL. 38 | 39 | ### Changed 40 | 41 | * Update default image version. 42 | * Perl: 5.38 43 | * PHP: 8.3 44 | * Node.js: 20.17.1 45 | * Remove "version" as it is no longer needed in docker compose v2. 46 | 47 | ## [2.6.0] - 2024-08-30 48 | 49 | ### Changed 50 | 51 | Changed the base image to "bento/ubuntu-24.04" when booting via `Vagrant mt-dev`. 52 | 53 | To update, you must delete the virtual machine with `vagrant destroy` and then start it again. Removing the virtual machine 54 | will result in the loss of all database data. If you do not update, you can continue to use 2.5.1 and there is no feature 55 | difference at this time. 56 | 57 | ## [2.5.1] - 2024-08-30 58 | 59 | ### Security 60 | 61 | * Install cpan modules from https://www.cpan.org 62 | 63 | ### Added 64 | 65 | * Add mt/mailpit.yml 66 | 67 | Enable to start MailPit service by the following command. 68 | ``` 69 | $ make up-psgi DOCKER_COMPOSE_USER_YAML="-f mt/mailpit.yml" 70 | ``` 71 | 72 | ## [2.5.0] - 2023-02-15 73 | 74 | ### Added 75 | 76 | * Enable to specify build command for each plugin 77 | 78 | ``` 79 | mt-plugin-MTBlockEditor: 80 | location: git@github.com:movabletype/mt-plugin-MTBlockEditor 81 | branch: develop 82 | build: 83 | command: 84 | - docker-compose 85 | - run 86 | - builder 87 | - bash 88 | - -c 89 | - 'perl Makefile.PL && make build' 90 | ``` 91 | 92 | ## [2.4.0] - 2023-11-25 93 | 94 | ### Changed 95 | 96 | ### Fixed 97 | 98 | * Run `CREATE DATABASE IF NOT EXISTS` only when `Database` is specified in `mt-config.cgi`. 99 | 100 | #### The command line has priority over the recipe 101 | 102 | As shown below, when "archive" is specified by a recipe and "ARCHIVE" is specified by a command line at the same time, the command line has priority over the recipe. 103 | 104 | ```yaml 105 | mt-plugin-MTBlockEditor: 106 | archive: 107 | url: https://github.com/movabletype/mt-plugin-MTBlockEditor/releases/download/v1.1.10/MTBlockEditor-1.1.10.tar.gz 108 | integrity: sha512-VCrI5B/cv4FAEV7O9GPOsJGEATwRcw4GqjVCWZiMPSkC9jx2l0kjnTXl6M2Xvv/x6THnPQj9VgxX9B0MG7a25g== 109 | ``` 110 | 111 | ``` 112 | $ make up RECIPE=8.0.0-dp ARCHIVE=MTBlockEditor-1.1.11.tar.gz 113 | ``` 114 | 115 | #### Enable to expose port from "mt" container 116 | 117 | In "mt" container, "httpd" or "psgi" listens on port 80, so specifying MT_EXPOSE_PORT will allow access to port 80 of the "mt" container from the port number specified in the host environment MT_EXPOSE_PORT. 118 | 119 | ``` 120 | $ make up-psgi MT_EXPOSE_PORT=5002 121 | ``` 122 | 123 | ## [2.3.2] - 2023-07-18 124 | 125 | ### Fixed 126 | 127 | * Fix compatibility issue, again. 128 | 129 | ## [2.3.1] - 2023-07-18 130 | 131 | ### Fixed 132 | 133 | * mt-dev now works in environments with Digest modules older than 1.17. 134 | 135 | ## [2.3.0] - 2023-07-13 136 | 137 | ### Added 138 | 139 | * Introduce UPDATE_DOCKER_IMAGE environment variable. 140 | * Setting this environment variable to "no" will skip updating the Docker image during `make up`. 141 | * ARCHIVE can now be specified in the recipe yaml file. 142 | * Specify url and integrity in the following format 143 | 144 | ```yaml 145 | mt-plugin-MTBlockEditor: 146 | archive: 147 | url: https://github.com/movabletype/mt-plugin-MTBlockEditor/releases/download/v1.1.10/MTBlockEditor-1.1.10.tar.gz 148 | integrity: sha512-VCrI5B/cv4FAEV7O9GPOsJGEATwRcw4GqjVCWZiMPSkC9jx2l0kjnTXl6M2Xvv/x6THnPQj9VgxX9B0MG7a25g== 149 | ``` 150 | 151 | ### Changed 152 | 153 | * Refactored Dockerfile for mt-watcher. 154 | * Ensure that binaries with the appropriate architecture are installed even if BuildKit is disabled. 155 | * Reduced image size by using "perl:*-slim" images. 156 | 157 | ### Fixed 158 | 159 | * Add workaround to run amd64 image on arm64. 160 | 161 | ## [2.2.0] - 2023-02-15 162 | 163 | ### Added 164 | 165 | * Can now be lunched in an arm64 environment. 166 | 167 | ## [2.1.2] - 2022-08-24 168 | 169 | ### Fixed 170 | 171 | * Ignore errors when deleting temporary files. 172 | * Use the `--pull` option to always use the latest image. 173 | * Accept "/" and "-" in branch names specified in `REPO`. 174 | 175 | ## [2.1.1] - 2022-06-20 176 | 177 | ### Fixed 178 | 179 | * Fixed an error with old docker-compose. 180 | 181 | ## [2.1.0] - 2022-06-18 182 | 183 | ### Added 184 | 185 | * Added support for specifying the cpanfile to be referenced at startup with DOCKER_MT_CPANFILES. 186 | * The default value is t/cpanfile. 187 | * CGIPath and StaticWebPath can now be specified relative to the host. 188 | * The database specified in mt-config.cgi is now automatically created if it does not exist. 189 | * If you do not want to create it automatically, you can skip this behavior by specifying `CREATE_DATABASE_IF_NOT_EXISTS=no`. 190 | * In the Vagrant environment, we have added a setting to forward the host's port to the guest. 191 | * The default value is 5825, which can be accessed at http://localhost:5825/cgi-bin/mt/mt.cgi. 192 | * You can change this value with the `VM_VB_HTTP_PORT` or `HTTPD_EXPOSE_PORT` environment variables. 193 | 194 | ## [2.0.0] - 2022-02-21 195 | 196 | ### Added 197 | 198 | #### Customized Docker containers 199 | 200 | You can launch the container with customizations of your choice. 201 | And the development container configuration for Visual Studio Code is included by default. 202 | 203 | ``` 204 | $ make up-psgi ... DOCKER_MT_DOCKERFILE=Dockerfile.devcontainer REPO="$HOME/src/github.com/username/mt-plugin-AwesomePlugin" 205 | ``` 206 | 207 | #### Support mount flag (especially for Docker for Mac) 208 | 209 | Docker for Mac has slow file access on bind mounts, but if you use a dev container, using :delegated may improve the situation. 210 | 211 | ``` 212 | $ make up-psgi ... DOCKER_VOLUME_MOUNT_FLAG=delegated 213 | ``` 214 | 215 | ### Changed 216 | 217 | * Also skip `git fetch` when "$UPDATE_BRANCH" is "no". 218 | * Rename environment variable DOCKER_COMPOSE_YML_MIDDLEWARES to DOCKER_COMPOSE_YAML_MIDDLEWARES 219 | 220 | ## [1.1.1] - 2022-01-03 221 | 222 | ### Changed 223 | 224 | * Change default private network. 225 | 226 | ### Added 227 | 228 | * Enable to start service via. 229 | * e.g. DOCKER\_MT\_SERVICES=postfix 230 | * Support docker-compose 2.x 231 | 232 | ### Fixed 233 | 234 | * Also watch the plugin directory specified by REPO. 235 | 236 | ## [1.1.0] - 2021-07-13 237 | 238 | ### Changed 239 | 240 | #### Removed some options for starman 241 | 242 | * Stop passing the -R option 243 | * https://metacpan.org/dist/Starman/view/script/starman#RELOADING-THE-APPLICATION 244 | * Stop passing the "-L Shotgun", because it is different from the option in general production environment. 245 | 246 | #### New file monitoring container 247 | 248 | * Added a container to monitor file updates and send a HUP signal to starman. 249 | 250 | [Details](https://github.com/movabletype/mt-dev/wiki/Architecture#mt-watcher) 251 | 252 | 253 | ## [1.0.7] - 2021-07-02 254 | 255 | ### Added 256 | 257 | * Enable mod\_include by default. 258 | * You can use SSI just choose "Apache Server-Side Include" in MT. 259 | 260 | ### Fixed 261 | 262 | * Improved stability when REPO is specified. 263 | 264 | ## [1.0.6] - 2021-05-20 265 | 266 | ### Added 267 | 268 | * Enable to use both RECIPE and ARCHIVE at the same time 269 | * e.g. RECIPE=7.8.0 ARCHIVE="https://github.com/movabletype/mt-plugin-MTBlockEditor/releases/download/v0.0.17-beta/MTBlockEditor-0.0.17-beta.tar.gz" 270 | 271 | ### Fixed 272 | 273 | * Fixed a bug when downloading multiple archives. 274 | 275 | ## [1.0.5] - 2021-05-19 276 | 277 | ### Added 278 | 279 | * Also link automatically "tools/*" of each plugins. 280 | * Enable to override branch by REPO 281 | * e.g. REPO="git@github.com:movabletype/movabletype#topic-branch" 282 | 283 | ### Fixed 284 | 285 | * Also prevent running `make clean me` when `UPDATE_BRANCH=no` is specified. 286 | * Invoke `apt` with DEBIAN_FRONTEND=noninteractive in provisioning. 287 | 288 | ## [1.0.4] - 2021-01-13 289 | 290 | ### Added 291 | 292 | * Enable to specify branch by "#" in REPO 293 | * e.g. REPO="https://github.com/user/mt-plugin-XXX.git#main" 294 | * Enable to specify branch by PR parameter 295 | * e.g. PR="https://github.com/movabletype/movabletype/pull/1527" 296 | 297 | ## [1.0.3] - 2020-10-22 298 | 299 | ### Added 300 | 301 | * Support relative CGIPath/StaticWebPath. 302 | * Specify NLS_LANG for suppoting Oracle Database. 303 | 304 | ## [1.0.2] - 2020-10-15 305 | 306 | ### Fixed 307 | 308 | * __BUILD_ID__ is now updated every time. 309 | 310 | ## [1.0.1] - 2020-08-27 311 | 312 | ### Added 313 | 314 | * Bind mt-dev directory to /mt-dev. 315 | 316 | ## [1.0.0] - 2020-06-22 317 | 318 | ### Changed 319 | 320 | * Use movabletype/test instead of movabletype/dev for docker image. 321 | 322 | ### Removed 323 | 324 | * We don't need to build php-5.3 as it can be verified on CentoOS 6. 325 | 326 | ## [0.0.11] - 2020-05-16 327 | 328 | ### Changed 329 | 330 | * Renewal local-repo feature as repo feature. 331 | 332 | ### Added 333 | 334 | * Enable to specify GIT URL to REPO variable. 335 | * Enable to specify ARCHIVE URL to ARCHIVE variable. 336 | 337 | ## [0.0.10] - 2020-05-16 338 | 339 | ### Changed 340 | 341 | * Removed "init-repo" target that doesn't needed. 342 | * Renewal ext-repos feature as local-repo feature. 343 | 344 | ## [0.0.9] - 2020-05-15 345 | 346 | ### Added 347 | 348 | * Added support for prefixed themes/plugin in the repository 349 | 350 | ## [0.0.8] - 2020-05-13 351 | 352 | ### Added 353 | 354 | * Add ext-repos feature. 355 | * Add cpan-install / cpan-uninstall command 356 | * Add cp-R command 357 | 358 | ### Fixed 359 | 360 | * Avoid errors in theme export 361 | 362 | ## [0.0.7] - 2020-04-16 363 | 364 | * Extend timeout for waiting response from MT at httpd. 365 | * Fix typo. 366 | 367 | ## [0.0.6] - 2020-04-01 368 | 369 | ### Added 370 | 371 | * Support shourtcut for perl/php/db docker image. 372 | 373 | ## [0.0.5] - 2020-04-01 374 | 375 | ### Added 376 | 377 | * Enable to invoke ./tools/*.pl by `vagrant mt-dev mt-shell`. 378 | 379 | ### Changed 380 | 381 | * Improve packup command wrapper. 382 | * Execute /usr/sbin/php-fpm on httpd container if available. 383 | * Use NFS for synced_folder on Mac. 384 | * Invoke `docker-compose pull` before each `docker-compose up`. 385 | * Invoke `make me` before run. 386 | 387 | ## [0.0.4] - 2020-03-17 388 | 389 | ### Added 390 | 391 | * Enable to keep the current branch. 392 | 393 | ### Fixed 394 | 395 | * Tweaks .env file feature. 396 | 397 | ## [0.0.3] - 2020-03-16 398 | 399 | ### Added 400 | 401 | * Support VSCode Remote Development 402 | 403 | ### Changed 404 | 405 | * Run starman with the auto reload option. 406 | 407 | ## [0.0.2] - 2020-03-11 408 | 409 | ### Added 410 | 411 | * Enable to specify both RECIPE and ARCHIVE. 412 | * `$ vagrant mt-dev up ARCHIVE=MTA7-R4605.tar.gz RECIPE=shared-preview` 413 | 414 | ### Changed 415 | 416 | * Update default software versions. 417 | * Perl : 5.28 418 | * PHP : 7.3 419 | * DB : MySQL 5.7 420 | 421 | ## [0.0.1] - 2020-03-09 422 | 423 | ### Added 424 | 425 | * Support multiple recipes. 426 | * `$ vagrant mt-dev up RECIPE=7.2,shared-preview` 427 | * Support custom mt-config.cgi 428 | * `$ vagrant mt-dev up RECIPE=7.2 MT_CONFIG_CGI=mt-config.cgi-7.2` 429 | 430 | ### Fixed 431 | 432 | * Fix repository pull bug. 433 | 434 | ## [0.0.0] - 2020-03-02 435 | 436 | Initial release 437 | -------------------------------------------------------------------------------- /bin/local/lib/perl5/Config/Tiny.pm: -------------------------------------------------------------------------------- 1 | package Config::Tiny; 2 | 3 | # If you thought Config::Simple was small... 4 | 5 | use strict; 6 | 7 | # Warning: There is another version line, in t/02.main.t. 8 | 9 | our $VERSION = '2.24'; 10 | 11 | BEGIN { 12 | require 5.008001; # For the utf8 stuff. 13 | $Config::Tiny::errstr = ''; 14 | } 15 | 16 | # Create an empty object. 17 | 18 | sub new { return bless {}, shift } 19 | 20 | # Create an object from a file. 21 | 22 | sub read 23 | { 24 | my($class) = ref $_[0] ? ref shift : shift; 25 | my($file, $encoding) = @_; 26 | 27 | return $class -> _error('No file name provided') if (! defined $file || ($file eq '') ); 28 | 29 | # Slurp in the file. 30 | 31 | $encoding = $encoding ? "<:$encoding" : '<'; 32 | local $/ = undef; 33 | 34 | open( CFG, $encoding, $file ) or return $class -> _error( "Failed to open file '$file' for reading: $!" ); 35 | my $contents = ; 36 | close( CFG ); 37 | 38 | return $class -> _error("Reading from '$file' returned undef") if (! defined $contents); 39 | 40 | return $class -> read_string( $contents ); 41 | 42 | } # End of read. 43 | 44 | # Create an object from a string. 45 | 46 | sub read_string 47 | { 48 | my($class) = ref $_[0] ? ref shift : shift; 49 | my($self) = bless {}, $class; 50 | 51 | return undef unless defined $_[0]; 52 | 53 | # Parse the file. 54 | 55 | my $ns = '_'; 56 | my $counter = 0; 57 | 58 | foreach ( split /(?:\015{1,2}\012|\015|\012)/, shift ) 59 | { 60 | $counter++; 61 | 62 | # Skip comments and empty lines. 63 | 64 | next if /^\s*(?:\#|\;|$)/; 65 | 66 | # Remove inline comments. 67 | 68 | s/\s\;\s.+$//g; 69 | 70 | # Handle section headers. 71 | 72 | if ( /^\s*\[\s*(.+?)\s*\]\s*$/ ) 73 | { 74 | # Create the sub-hash if it doesn't exist. 75 | # Without this sections without keys will not 76 | # appear at all in the completed struct. 77 | 78 | $self->{$ns = $1} ||= {}; 79 | 80 | next; 81 | } 82 | 83 | # Handle properties. 84 | 85 | if ( /^\s*([^=]+?)\s*=\s*(.*?)\s*$/ ) 86 | { 87 | $self->{$ns}->{$1} = $2; 88 | 89 | next; 90 | } 91 | 92 | return $self -> _error( "Syntax error at line $counter: '$_'" ); 93 | } 94 | 95 | return $self; 96 | } 97 | 98 | # Save an object to a file. 99 | 100 | sub write 101 | { 102 | my($self) = shift; 103 | my($file, $encoding) = @_; 104 | 105 | return $self -> _error('No file name provided') if (! defined $file or ($file eq '') ); 106 | 107 | $encoding = $encoding ? ">:$encoding" : '>'; 108 | 109 | # Write it to the file. 110 | 111 | my($string) = $self->write_string; 112 | 113 | return undef unless defined $string; 114 | 115 | open( CFG, $encoding, $file ) or return $self->_error("Failed to open file '$file' for writing: $!"); 116 | print CFG $string; 117 | close CFG; 118 | 119 | return 1; 120 | 121 | } # End of write. 122 | 123 | # Save an object to a string. 124 | 125 | sub write_string 126 | { 127 | my($self) = shift; 128 | my($contents) = ''; 129 | 130 | for my $section ( sort { (($b eq '_') <=> ($a eq '_')) || ($a cmp $b) } keys %$self ) 131 | { 132 | # Check for several known-bad situations with the section 133 | # 1. Leading whitespace 134 | # 2. Trailing whitespace 135 | # 3. Newlines in section name. 136 | 137 | return $self->_error("Illegal whitespace in section name '$section'") if $section =~ /(?:^\s|\n|\s$)/s; 138 | 139 | my $block = $self->{$section}; 140 | $contents .= "\n" if length $contents; 141 | $contents .= "[$section]\n" unless $section eq '_'; 142 | 143 | for my $property ( sort keys %$block ) 144 | { 145 | return $self->_error("Illegal newlines in property '$section.$property'") if $block->{$property} =~ /(?:\012|\015)/s; 146 | 147 | $contents .= "$property=$block->{$property}\n"; 148 | } 149 | } 150 | 151 | return $contents; 152 | 153 | } # End of write_string. 154 | 155 | # Error handling. 156 | 157 | sub errstr { $Config::Tiny::errstr } 158 | sub _error { $Config::Tiny::errstr = $_[1]; undef } 159 | 160 | 1; 161 | 162 | __END__ 163 | 164 | =pod 165 | 166 | =head1 NAME 167 | 168 | Config::Tiny - Read/Write .ini style files with as little code as possible 169 | 170 | =head1 SYNOPSIS 171 | 172 | # In your configuration file 173 | rootproperty=blah 174 | 175 | [section] 176 | one=twp 177 | three= four 178 | Foo =Bar 179 | empty= 180 | 181 | # In your program 182 | use Config::Tiny; 183 | 184 | # Create a config 185 | my $Config = Config::Tiny->new; 186 | 187 | # Open the config 188 | $Config = Config::Tiny->read( 'file.conf' ); 189 | $Config = Config::Tiny->read( 'file.conf', 'utf8' ); # Neither ':' nor '<:' prefix! 190 | $Config = Config::Tiny->read( 'file.conf', 'encoding(iso-8859-1)'); 191 | 192 | # Reading properties 193 | my $rootproperty = $Config->{_}->{rootproperty}; 194 | my $one = $Config->{section}->{one}; 195 | my $Foo = $Config->{section}->{Foo}; 196 | 197 | # Changing data 198 | $Config->{newsection} = { this => 'that' }; # Add a section 199 | $Config->{section}->{Foo} = 'Not Bar!'; # Change a value 200 | delete $Config->{_}; # Delete a value or section 201 | 202 | # Save a config 203 | $Config->write( 'file.conf' ); 204 | $Config->write( 'file.conf', 'utf8' ); # Neither ':' nor '>:' prefix! 205 | 206 | # Shortcuts 207 | my($rootproperty) = $$Config{_}{rootproperty}; 208 | 209 | my($config) = Config::Tiny -> read_string('alpha=bet'); 210 | my($value) = $$config{_}{alpha}; # $value is 'bet'. 211 | 212 | my($config) = Config::Tiny -> read_string("[init]\nalpha=bet"); 213 | my($value) = $$config{init}{alpha}; # $value is 'bet'. 214 | 215 | =head1 DESCRIPTION 216 | 217 | C is a Perl class to read and write .ini style configuration 218 | files with as little code as possible, reducing load time and memory overhead. 219 | 220 | Most of the time it is accepted that Perl applications use a lot of memory and modules. 221 | 222 | The C<*::Tiny> family of modules is specifically intended to provide an ultralight alternative 223 | to the standard modules. 224 | 225 | This module is primarily for reading human written files, and anything we write shouldn't need to 226 | have documentation/comments. If you need something with more power move up to L, 227 | L or one of the many other C modules. 228 | 229 | Lastly, L does B preserve your comments, whitespace, or the order of your config 230 | file. 231 | 232 | See L (and possibly others) for the preservation of the order of the entries 233 | in the file. 234 | 235 | =head1 CONFIGURATION FILE SYNTAX 236 | 237 | Files are the same format as for MS Windows C<*.ini> files. For example: 238 | 239 | [section] 240 | var1=value1 241 | var2=value2 242 | 243 | If a property is outside of a section at the beginning of a file, it will 244 | be assigned to the C<"root section">, available at C<$Config-E{_}>. 245 | 246 | Lines starting with C<'#'> or C<';'> are considered comments and ignored, 247 | as are blank lines. 248 | 249 | When writing back to the config file, all comments, custom whitespace, 250 | and the ordering of your config file elements are discarded. If you need 251 | to keep the human elements of a config when writing back, upgrade to 252 | something better, this module is not for you. 253 | 254 | =head1 METHODS 255 | 256 | =head2 errstr() 257 | 258 | Returns a string representing the most recent error, or the empty string. 259 | 260 | You can also retrieve the error message from the C<$Config::Tiny::errstr> variable. 261 | 262 | =head2 new() 263 | 264 | The constructor C creates and returns an empty C object. 265 | 266 | =head2 read($filename, [$encoding]) 267 | 268 | Here, the [] indicate an optional parameter. 269 | 270 | The C constructor reads a config file, $filename, and returns a new 271 | C object containing the properties in the file. 272 | 273 | $encoding may be used to indicate the encoding of the file, e.g. 'utf8' or 'encoding(iso-8859-1)'. 274 | 275 | Do not add a prefix to $encoding, such as '<' or '<:'. 276 | 277 | Returns the object on success, or C on error. 278 | 279 | When C fails, C sets an error message internally 280 | you can recover via Cerrstr>. Although in B 281 | cases a failed C will also set the operating system error 282 | variable C<$!>, not all errors do and you should not rely on using 283 | the C<$!> variable. 284 | 285 | See t/04.utf8.t and t/04.utf8.txt. 286 | 287 | =head2 read_string($string) 288 | 289 | The C method takes as argument the contents of a config file 290 | as a string and returns the C object for it. 291 | 292 | =head2 write($filename, [$encoding]) 293 | 294 | Here, the [] indicate an optional parameter. 295 | 296 | The C method generates the file content for the properties, and 297 | writes it to disk to the filename specified. 298 | 299 | $encoding may be used to indicate the encoding of the file, e.g. 'utf8' or 'encoding(iso-8859-1)'. 300 | 301 | Do not add a prefix to $encoding, such as '>' or '>:'. 302 | 303 | Returns true on success or C on error. 304 | 305 | See t/04.utf8.t and t/04.utf8.txt. 306 | 307 | =head2 write_string() 308 | 309 | Generates the file content for the object and returns it as a string. 310 | 311 | =head1 FAQ 312 | 313 | =head2 What happens if a key is repeated? 314 | 315 | The last value is retained, overwriting any previous values. 316 | 317 | See t/06.repeat.key.t. 318 | 319 | =head2 Why can't I put comments at the ends of lines? 320 | 321 | =over 4 322 | 323 | =item o The # char is only introduces a comment when it's at the start of a line. 324 | 325 | So a line like: 326 | 327 | key=value # A comment 328 | 329 | Sets key to 'value # A comment', which, presumably, you did not intend. 330 | 331 | This conforms to the syntax discussed in L. 332 | 333 | =item o Comments matching /\s\;\s.+$//g; are ignored. 334 | 335 | This means you can't preserve the suffix using: 336 | 337 | key = Prefix ; Suffix 338 | 339 | Result: key is now 'Prefix'. 340 | 341 | But you can do this: 342 | 343 | key = Prefix;Suffix 344 | 345 | Result: key is now 'Prefix;Suffix'. 346 | 347 | Or this: 348 | 349 | key = Prefix; Suffix 350 | 351 | Result: key is now 'Prefix; Suffix'. 352 | 353 | =back 354 | 355 | See t/07.trailing.comment.t. 356 | 357 | =head2 Why can't I omit the '=' signs? 358 | 359 | E.g.: 360 | 361 | [Things] 362 | my = 363 | list = 364 | of = 365 | things = 366 | 367 | Instead of: 368 | 369 | [Things] 370 | my 371 | list 372 | of 373 | things 374 | 375 | Because the use of '=' signs is a type of mandatory documentation. It indicates that that section 376 | contains 4 items, and not 1 odd item split over 4 lines. 377 | 378 | =head2 Why do I have to assign the result of a method call to a variable? 379 | 380 | This question comes from RT#85386. 381 | 382 | Yes, the syntax may seem odd, but you don't have to call both new() and read_string(). 383 | 384 | Try: 385 | 386 | perl -MData::Dumper -MConfig::Tiny -E 'my $c=Config::Tiny->read_string("one=s"); say Dumper $c' 387 | 388 | Or: 389 | 390 | my($config) = Config::Tiny -> read_string('alpha=bet'); 391 | my($value) = $$config{_}{alpha}; # $value is 'bet'. 392 | 393 | Or even, a bit ridiculously: 394 | 395 | my($value) = ${Config::Tiny -> read_string('alpha=bet')}{_}{alpha}; # $value is 'bet'. 396 | 397 | =head2 Can I use a file called '0' (zero)? 398 | 399 | Yes. See t/05.zero.t (test code) and t/0 (test data). 400 | 401 | =head1 CAVEATS 402 | 403 | Some edge cases in section headers are not supported, and additionally may not 404 | be detected when writing the config file. 405 | 406 | Specifically, section headers with leading whitespace, trailing whitespace, 407 | or newlines anywhere in the section header, will not be written correctly 408 | to the file and may cause file corruption. 409 | 410 | =head1 Repository 411 | 412 | L 413 | 414 | =head1 SUPPORT 415 | 416 | Bugs should be reported via the CPAN bug tracker at 417 | 418 | L 419 | 420 | For other issues, or commercial enhancement or support, contact the author. 421 | 422 | =head1 AUTHOR 423 | 424 | Adam Kennedy Eadamk@cpan.orgE 425 | 426 | Maintanence from V 2.15: Ron Savage L. 427 | 428 | =head1 ACKNOWLEGEMENTS 429 | 430 | Thanks to Sherzod Ruzmetov Esherzodr@cpan.orgE for 431 | L, which inspired this module by being not quite 432 | "simple" enough for me :). 433 | 434 | =head1 SEE ALSO 435 | 436 | See, amongst many: L and L. 437 | 438 | See L (and possibly others) for the preservation of the order of the entries 439 | in the file. 440 | 441 | L. Ini On Drugs. 442 | 443 | L 444 | 445 | L 446 | 447 | L 448 | 449 | L. Config data from Perl itself. 450 | 451 | L 452 | 453 | L 454 | 455 | L 456 | 457 | L. Allows nested data. 458 | 459 | L. Author: RJBS. Uses Moose. Extremely complex. 460 | 461 | L. See next few lines: 462 | 463 | L 464 | 465 | L. 1 Star rating. 466 | 467 | L 468 | 469 | =head1 COPYRIGHT 470 | 471 | Copyright 2002 - 2011 Adam Kennedy. 472 | 473 | This program is free software; you can redistribute 474 | it and/or modify it under the same terms as Perl itself. 475 | 476 | The full text of the license can be found in the 477 | LICENSE file included with this module. 478 | 479 | =cut 480 | -------------------------------------------------------------------------------- /bin/setup-environment: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env perl 2 | 3 | use strict; 4 | use warnings; 5 | use utf8; 6 | 7 | use FindBin; 8 | use lib "$FindBin::Bin/local/lib/perl5"; 9 | 10 | use Cwd qw(realpath getcwd); 11 | use File::Path qw(mkpath); 12 | use IO::File; 13 | use IPC::Open3 qw(open3); 14 | use JSON::PP qw(encode_json); 15 | use Data::Dumper; 16 | use Digest::MD5; 17 | use Digest::SHA; 18 | use Getopt::Long; 19 | use HTTP::Tiny; 20 | use Config::Tiny; 21 | use YAML::Tiny qw(LoadFile Dump); 22 | use File::Basename qw(basename fileparse dirname); 23 | use File::Spec::Functions qw(catfile); 24 | use File::Temp qw(tempfile tmpnam tempdir); 25 | 26 | GetOptions( 27 | 'recipe=s' => \my $recipes, 28 | 'repo=s' => \my $repos, 29 | 'pr=s' => \my $pull_reqs, 30 | 'archive=s' => \my $archives, 31 | ); 32 | 33 | my $recipe_dir = realpath("$FindBin::Bin/../recipe"); 34 | my $repo_dir = realpath("$FindBin::Bin/../repo"); 35 | my $repo_base_dir = realpath("$FindBin::Bin/../.."); 36 | my $archive_dir = realpath("$FindBin::Bin/../archive"); 37 | my $git_clone_cmd = realpath("$FindBin::Bin/git-clone"); 38 | 39 | my $env = {}; 40 | my $yamls = []; 41 | my $workspace = { 42 | folders => [], 43 | settings => {}, 44 | }; 45 | my $pr_is_used; 46 | 47 | sub _check_integrity { 48 | my ($integrity, $file_or_content) = @_; 49 | 50 | my ($algorithm) = $integrity =~ m{\Asha(\d+)}; 51 | my $sha = Digest::SHA->new($algorithm); 52 | if (ref $file_or_content) { 53 | 54 | $sha->addfile($file_or_content); 55 | } else { 56 | $sha->add($file_or_content); 57 | } 58 | my $digest = $sha->b64digest; 59 | $digest .= ('=' x ((4 - (length($digest) % 4)) % 4)); 60 | my $file_integrity = "sha256-" . $digest; 61 | 62 | return $integrity eq ("sha$algorithm-" . $digest); 63 | } 64 | 65 | sub handle_recipe { 66 | my $data = shift; 67 | 68 | # detect docker images 69 | if ( my $perl = delete $data->{perl} ) { 70 | $env->{DOCKER_MT_IMAGE} = "movabletype/test:perl-$perl"; 71 | } 72 | if ( my $mysql = delete $data->{database} ) { 73 | $env->{DOCKER_MYSQL_IMAGE} = $mysql; 74 | } 75 | if ( my $php = delete $data->{php} ) { 76 | $env->{DOCKER_HTTPD_IMAGE} = "movabletype/test:php-$php"; 77 | } 78 | 79 | # checout branch and collect links 80 | my @volumes; 81 | for my $k ( keys %$data ) { 82 | if (my $archive = $data->{$k}{archive}) { 83 | my $url = $archive->{url}; 84 | my $integrity = $archive->{integrity}; 85 | 86 | die "archive url and integrity is required: $k" 87 | unless $url && $integrity; 88 | 89 | my $basename = basename($url); 90 | my $rel_path = catfile($k, $basename); 91 | my $full_path = catfile($archive_dir, $rel_path); 92 | 93 | if (-f $full_path && _check_integrity($integrity, IO::File->new($full_path, 'r'))) { 94 | $archives = "$rel_path $archives"; 95 | next; 96 | } 97 | 98 | mkpath(dirname($full_path)); 99 | my $response = HTTP::Tiny->new->get($url); 100 | die "Failed: @{[Dumper($response)]}" 101 | unless $response->{success} && length $response->{content}; 102 | 103 | if (!_check_integrity($integrity, $response->{content})) { 104 | die "intergrity check failed: $url"; 105 | } 106 | 107 | my $fh = IO::File->new($full_path, 'w') 108 | or die "Can't open $full_path: $!"; 109 | print $fh $response->{content}; 110 | close $fh; 111 | 112 | $archives = "$rel_path $archives"; 113 | 114 | next; 115 | } 116 | 117 | my $repo = $data->{$k}{location}; 118 | if (($ENV{CODESPACES} || '') eq 'true') { 119 | $repo =~ s{^git\@github.com:}{https://github.com/}; 120 | } 121 | my $dest_dir = $data->{$k}{directory} || do { 122 | "$repo_base_dir/@{[basename($repo)]}"; 123 | }; 124 | if ( $dest_dir !~ m{\A/} ) { 125 | $dest_dir = "$repo_base_dir/$dest_dir"; 126 | } 127 | ( my $basename = $dest_dir ) =~ s{.*/}{}; 128 | 129 | if ( $k eq 'core' ) { 130 | $env->{MT_HOME_PATH} = $dest_dir; 131 | } 132 | 133 | if ($repo) { 134 | my $branch = $data->{$k}{branch} || ''; 135 | 136 | for my $pr ( split /\s+/, $pull_reqs ) { 137 | my ( $pr_repo, $pr_id ) = ( $pr =~ m{([^/]+)/(pull/\d+)} ); 138 | next unless $pr_repo && $pr_id; 139 | if ($basename eq $pr_repo) { 140 | $branch = $pr_id . '/head'; 141 | $pr_is_used = 1; 142 | } 143 | } 144 | 145 | { 146 | my %repos = map { $_ => 1 } split /\s+/, $repos; 147 | for my $r ( keys %repos ) { 148 | my ( $bn, $br ) = $r =~ m{([^/]+)#(.*)$} 149 | or next; 150 | 151 | next unless $basename eq $bn; 152 | 153 | $branch = $br; 154 | delete $repos{$r}; 155 | } 156 | $repos = join ' ', keys %repos; 157 | } 158 | 159 | my $cmd = "$git_clone_cmd $repo $dest_dir $branch"; 160 | my $res = `$cmd 2>&1` || ""; 161 | die "$res\nGot an error: $cmd" if $?; 162 | 163 | $res = `cd $dest_dir && git rev-parse HEAD 2>&1`; 164 | die "$res\nGot an error: git rev-parse HEAD" if $?; 165 | print STDERR "$k:$res"; 166 | 167 | my $build_cmd = $data->{$k}{build}{command}; 168 | if ($build_cmd) { 169 | my $cur_dir = getcwd(); 170 | chdir $dest_dir; 171 | my ($res, $build_cmd_str); 172 | if (ref $build_cmd eq 'ARRAY') { 173 | my $pid = open3( my $in, my $out, my $err, @$build_cmd ); 174 | $res .= join('', <$out>) if $out; 175 | $res .= join('', <$err>) if $err; 176 | waitpid($pid, 0); 177 | $build_cmd_str = join(' ', @$build_cmd); 178 | } 179 | else { 180 | $res = `$build_cmd 2>&1`; 181 | $build_cmd_str = $build_cmd; 182 | } 183 | chdir $cur_dir; 184 | die "$res\nGot an error: $build_cmd_str" if $?; 185 | } 186 | } 187 | 188 | push @volumes, " - '$dest_dir:/src/$basename:\${DOCKER_VOLUME_MOUNT_FLAG:-rw}'"; 189 | push @{ $workspace->{folders} }, { path => $basename, }; 190 | 191 | next if $k eq 'core'; 192 | 193 | my $plugin_dir = $dest_dir; 194 | if ( $data->{$k}{prefix} ) { 195 | $plugin_dir = catfile( $plugin_dir, $data->{$k}{prefix} ); 196 | } 197 | 198 | my $links = $data->{$k}{links}; 199 | if ( !$links ) { 200 | $links = [ 201 | map { $_ =~ s{\A\Q$plugin_dir\E/}{}; $_ } ( 202 | glob("$plugin_dir/*.cgi"), 203 | glob("$plugin_dir/plugins/*"), 204 | glob("$plugin_dir/mt-static/plugins/*"), 205 | glob("$plugin_dir/themes/*"), 206 | glob("$plugin_dir/addons/*"), 207 | glob("$plugin_dir/mt-static/addons/*"), 208 | glob("$plugin_dir/tools/*"), 209 | ) 210 | ]; 211 | } 212 | 213 | for my $l (@$links) { 214 | push @volumes, 215 | " - '$plugin_dir/$l:/var/www/cgi-bin/mt/$l:\${DOCKER_VOLUME_MOUNT_FLAG:-rw}'"; 216 | } 217 | } 218 | 219 | return unless @volumes; 220 | 221 | my ( $fh, $file ) = tmpnam(); 222 | print $fh <read( $conf_file, 'utf8' ); 257 | if ( !$conf ) { 258 | $conf = Config::Tiny->new; 259 | 260 | print STDERR "Please input base URL of recipe data: "; 261 | my $base = ; 262 | chomp $base; 263 | 264 | $conf->{recipe}{base_url} = $base; 265 | $conf->write( $conf_file, 'utf8' ); 266 | } 267 | 268 | if ( $conf->{recipe}{base_url} ) { 269 | my $base_url = $conf->{recipe}{base_url}; 270 | $base_url =~ s/\/+$//; 271 | "$base_url/$recipe"; 272 | } 273 | else { 274 | ""; 275 | } 276 | }; 277 | 278 | if ($recipe_url) { 279 | my $response = HTTP::Tiny->new->get($recipe_url); 280 | die "Failed: @{[Dumper($response)]}" 281 | unless $response->{success} && length $response->{content}; 282 | 283 | my ( $fh, $filename ) = tempfile(); 284 | print $fh $response->{content}; 285 | 286 | $recipe_file = $filename; 287 | } 288 | 289 | if ( !$recipe_file ) { 290 | die qq{Can not find recipe for "$recipe".}; 291 | } 292 | } 293 | 294 | handle_recipe( LoadFile($recipe_file) ); 295 | } 296 | 297 | for my $r ( split /\s+/, $repos ) { 298 | my $directory 299 | = ( $r =~ m{^[/\.]} && -d $r ) 300 | ? realpath($r) 301 | : "$repo_dir/$r"; 302 | 303 | next unless $directory; 304 | 305 | # get repo 306 | if ( !-d $directory ) { 307 | my $branch = ''; 308 | $branch = $1 if $r =~ s{(?:#([\w/-]+))\z}{}; 309 | 310 | my $k = basename($r); 311 | $directory = catfile( $repo_base_dir, $k ); 312 | 313 | my $cmd = "$git_clone_cmd $r $directory $branch"; 314 | my $res = `$cmd 2>&1` || ""; 315 | die "$res\nGot an error: $cmd" if $?; 316 | 317 | $res = `cd $directory && git rev-parse HEAD 2>&1`; 318 | die "$res\nGot an error: git rev-parse HEAD" if $?; 319 | print STDERR "$k:$res"; 320 | } 321 | 322 | handle_recipe( { $r => { directory => $directory, }, } ); 323 | } 324 | 325 | for my $a ( split /\s+/, $archives ) { 326 | next unless $a; 327 | 328 | my $file_path; 329 | my ( $basename, $ext ) = ( fileparse( $a, qr/\.[a-zA-Z\.]+$/ ) )[ 0, 2 ]; 330 | 331 | if ( $a =~ m{^https?://} ) { 332 | 333 | # download 334 | my $response = HTTP::Tiny->new->get($a); 335 | 336 | if ( 337 | $response->{status} == 404 338 | && system('gh auth status > /dev/null 2>&1') == 0 339 | && $a =~ m{^https://github.com/([^/]+/[^/]+)/actions/runs/[0-9]+/artifacts/([0-9]+)$} 340 | ) { 341 | my $repo = $1; 342 | my $artifact_id = $2; 343 | 344 | my $cwd = getcwd(); 345 | my $tmp_dir = tempdir( '/tmp/mt-dev-archive-temp-XXXXX', ); 346 | 347 | chdir $tmp_dir; 348 | 349 | my $cmd = "gh api repos/$repo/actions/artifacts/$artifact_id/zip > archive.zip"; 350 | my $res = `$cmd 2>&1` || ""; 351 | die "$res\nGot an error: $cmd" if $?; 352 | 353 | $cmd = "unzip -q archive.zip"; 354 | $res = `$cmd 2>&1` || ""; 355 | die "$res\nGot an error: $cmd" if $?; 356 | 357 | unlink 'archive.zip'; 358 | my ($filename) = `ls`; 359 | chomp $filename; 360 | 361 | chdir $cwd; 362 | 363 | $file_path = catfile( $tmp_dir, $filename ); 364 | } 365 | elsif ( !$response->{success} || !$response->{content} ) { 366 | die "Failed: @{[Dumper($response)]}"; 367 | } 368 | else { 369 | ( my $fh, $file_path ) = tempfile( undef, SUFFIX => $ext ); 370 | print $fh $response->{content}; 371 | } 372 | } 373 | elsif ( $a =~ m{^/} ) { 374 | $file_path = $a; 375 | } 376 | else { 377 | $file_path = catfile( $archive_dir, $a ); 378 | } 379 | 380 | die "File not found: $a" unless -f $file_path; 381 | 382 | # extract archive 383 | my $cwd = getcwd(); 384 | my $directory = tempdir( '/tmp/mt-dev-archive-temp-XXXXX', ); 385 | 386 | chdir $directory; 387 | my $extract_res 388 | = $file_path =~ m/\.zip$/i 389 | ? `unzip -q $file_path` 390 | : `tar zxf $file_path`; 391 | if ($?) { 392 | die "Failed to extract: $a"; 393 | } 394 | 395 | my $md5_ctx = Digest::MD5->new; 396 | $md5_ctx->addfile( IO::File->new( $file_path, 'r' ) ); 397 | print STDERR join( ':', $basename . $ext, $md5_ctx->hexdigest ) . "\n"; 398 | 399 | my @entries = grep { 400 | !m{^(addons|mt-static|php|alt-tmpl|plugins|default_templates|search_templates|extlib|themes|import|tmpl|tools|lib|.*\.cgi)$} 401 | } glob('*'); 402 | if ( @entries == 1 ) { 403 | $directory = catfile( $directory, $entries[0] ); 404 | } 405 | chdir $cwd; 406 | 407 | $basename =~ s{\.}{_}g; # "." causes problems in yaml files 408 | handle_recipe( { $basename => { directory => $directory, }, } ); 409 | } 410 | 411 | if ($pull_reqs && !$pr_is_used) { 412 | die "specified pr option is not used: ${pull_reqs}"; 413 | } 414 | 415 | { 416 | my ( $ws_fh, $ws_file ) = tmpnam(); 417 | print $ws_fh encode_json($workspace); 418 | close $ws_fh; 419 | 420 | my ( $fh, $file ) = tmpnam(); 421 | print $fh Dump({ 422 | services => { 423 | map { 424 | $_ => { volumes => ["$ws_file:/src/mt.code-workspace",], }, 425 | } qw(mt httpd) 426 | }, 427 | }); 428 | push @$yamls, $file; 429 | } 430 | 431 | if (@$yamls) { 432 | $env->{DOCKER_COMPOSE_YAML_OVERRIDE} = join( ' ', map {"-f $_"} @$yamls ); 433 | } 434 | 435 | # output args for make command 436 | print join ' ', map {qq{$_="@{[$env->{$_}]}"}} 437 | keys(%$env); 438 | -------------------------------------------------------------------------------- /mt/httpd/devcontainer/build-script/common-debian.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | #------------------------------------------------------------------------------------------------------------- 3 | # Copyright (c) Microsoft Corporation. All rights reserved. 4 | # Licensed under the MIT License. See https://go.microsoft.com/fwlink/?linkid=2090316 for license information. 5 | #------------------------------------------------------------------------------------------------------------- 6 | # 7 | # Docs: https://github.com/microsoft/vscode-dev-containers/blob/main/script-library/docs/common.md 8 | # Maintainer: The VS Code and Codespaces Teams 9 | # 10 | # Syntax: ./common-debian.sh [install zsh flag] [username] [user UID] [user GID] [upgrade packages flag] [install Oh My Zsh! flag] [Add non-free packages] 11 | 12 | set -e 13 | 14 | INSTALL_ZSH=${1:-"true"} 15 | USERNAME=${2:-"automatic"} 16 | USER_UID=${3:-"automatic"} 17 | USER_GID=${4:-"automatic"} 18 | UPGRADE_PACKAGES=${5:-"true"} 19 | INSTALL_OH_MYS=${6:-"true"} 20 | ADD_NON_FREE_PACKAGES=${7:-"false"} 21 | SCRIPT_DIR="$(cd $(dirname "${BASH_SOURCE[0]}") && pwd)" 22 | MARKER_FILE="/usr/local/etc/vscode-dev-containers/common" 23 | 24 | if [ "$(id -u)" -ne 0 ]; then 25 | echo -e 'Script must be run as root. Use sudo, su, or add "USER root" to your Dockerfile before running this script.' 26 | exit 1 27 | fi 28 | 29 | # Ensure that login shells get the correct path if the user updated the PATH using ENV. 30 | rm -f /etc/profile.d/00-restore-env.sh 31 | echo "export PATH=${PATH//$(sh -lc 'echo $PATH')/\$PATH}" > /etc/profile.d/00-restore-env.sh 32 | chmod +x /etc/profile.d/00-restore-env.sh 33 | 34 | # If in automatic mode, determine if a user already exists, if not use vscode 35 | if [ "${USERNAME}" = "auto" ] || [ "${USERNAME}" = "automatic" ]; then 36 | USERNAME="" 37 | POSSIBLE_USERS=("vscode" "node" "codespace" "$(awk -v val=1000 -F ":" '$3==val{print $1}' /etc/passwd)") 38 | for CURRENT_USER in ${POSSIBLE_USERS[@]}; do 39 | if id -u ${CURRENT_USER} > /dev/null 2>&1; then 40 | USERNAME=${CURRENT_USER} 41 | break 42 | fi 43 | done 44 | if [ "${USERNAME}" = "" ]; then 45 | USERNAME=vscode 46 | fi 47 | elif [ "${USERNAME}" = "none" ]; then 48 | USERNAME=root 49 | USER_UID=0 50 | USER_GID=0 51 | fi 52 | 53 | # Load markers to see which steps have already run 54 | if [ -f "${MARKER_FILE}" ]; then 55 | echo "Marker file found:" 56 | cat "${MARKER_FILE}" 57 | source "${MARKER_FILE}" 58 | fi 59 | 60 | # Ensure apt is in non-interactive to avoid prompts 61 | export DEBIAN_FRONTEND=noninteractive 62 | 63 | # Function to call apt-get if needed 64 | apt_get_update_if_needed() 65 | { 66 | if [ ! -d "/var/lib/apt/lists" ] || [ "$(ls /var/lib/apt/lists/ | wc -l)" = "0" ]; then 67 | echo "Running apt-get update..." 68 | apt-get update 69 | else 70 | echo "Skipping apt-get update." 71 | fi 72 | } 73 | 74 | # Run install apt-utils to avoid debconf warning then verify presence of other common developer tools and dependencies 75 | if [ "${PACKAGES_ALREADY_INSTALLED}" != "true" ]; then 76 | 77 | package_list="apt-utils \ 78 | openssh-client \ 79 | gnupg2 \ 80 | dirmngr \ 81 | iproute2 \ 82 | procps \ 83 | lsof \ 84 | htop \ 85 | net-tools \ 86 | psmisc \ 87 | curl \ 88 | wget \ 89 | rsync \ 90 | ca-certificates \ 91 | unzip \ 92 | zip \ 93 | nano \ 94 | vim-tiny \ 95 | less \ 96 | jq \ 97 | lsb-release \ 98 | apt-transport-https \ 99 | dialog \ 100 | libc6 \ 101 | libgcc1 \ 102 | libkrb5-3 \ 103 | libgssapi-krb5-2 \ 104 | libicu[0-9][0-9] \ 105 | liblttng-ust0 \ 106 | libstdc++6 \ 107 | zlib1g \ 108 | locales \ 109 | sudo \ 110 | ncdu \ 111 | man-db \ 112 | strace \ 113 | manpages \ 114 | manpages-dev \ 115 | init-system-helpers" 116 | 117 | # Needed for adding manpages-posix and manpages-posix-dev which are non-free packages in Debian 118 | if [ "${ADD_NON_FREE_PACKAGES}" = "true" ]; then 119 | # Bring in variables from /etc/os-release like VERSION_CODENAME 120 | . /etc/os-release 121 | sed -i -E "s/deb http:\/\/(deb|httpredir)\.debian\.org\/debian ${VERSION_CODENAME} main/deb http:\/\/\1\.debian\.org\/debian ${VERSION_CODENAME} main contrib non-free/" /etc/apt/sources.list 122 | sed -i -E "s/deb-src http:\/\/(deb|httredir)\.debian\.org\/debian ${VERSION_CODENAME} main/deb http:\/\/\1\.debian\.org\/debian ${VERSION_CODENAME} main contrib non-free/" /etc/apt/sources.list 123 | sed -i -E "s/deb http:\/\/(deb|httpredir)\.debian\.org\/debian ${VERSION_CODENAME}-updates main/deb http:\/\/\1\.debian\.org\/debian ${VERSION_CODENAME}-updates main contrib non-free/" /etc/apt/sources.list 124 | sed -i -E "s/deb-src http:\/\/(deb|httpredir)\.debian\.org\/debian ${VERSION_CODENAME}-updates main/deb http:\/\/\1\.debian\.org\/debian ${VERSION_CODENAME}-updates main contrib non-free/" /etc/apt/sources.list 125 | sed -i "s/deb http:\/\/security\.debian\.org\/debian-security ${VERSION_CODENAME}\/updates main/deb http:\/\/security\.debian\.org\/debian-security ${VERSION_CODENAME}\/updates main contrib non-free/" /etc/apt/sources.list 126 | sed -i "s/deb-src http:\/\/security\.debian\.org\/debian-security ${VERSION_CODENAME}\/updates main/deb http:\/\/security\.debian\.org\/debian-security ${VERSION_CODENAME}\/updates main contrib non-free/" /etc/apt/sources.list 127 | sed -i "s/deb http:\/\/deb\.debian\.org\/debian ${VERSION_CODENAME}-backports main/deb http:\/\/deb\.debian\.org\/debian ${VERSION_CODENAME}-backports main contrib non-free/" /etc/apt/sources.list 128 | sed -i "s/deb-src http:\/\/deb\.debian\.org\/debian ${VERSION_CODENAME}-backports main/deb http:\/\/deb\.debian\.org\/debian ${VERSION_CODENAME}-backports main contrib non-free/" /etc/apt/sources.list 129 | # Handle bullseye location for security https://www.debian.org/releases/bullseye/amd64/release-notes/ch-information.en.html 130 | sed -i "s/deb http:\/\/security\.debian\.org\/debian-security ${VERSION_CODENAME}-security main/deb http:\/\/security\.debian\.org\/debian-security ${VERSION_CODENAME}-security main contrib non-free/" /etc/apt/sources.list 131 | sed -i "s/deb-src http:\/\/security\.debian\.org\/debian-security ${VERSION_CODENAME}-security main/deb http:\/\/security\.debian\.org\/debian-security ${VERSION_CODENAME}-security main contrib non-free/" /etc/apt/sources.list 132 | echo "Running apt-get update..." 133 | apt-get update 134 | package_list="${package_list} manpages-posix manpages-posix-dev" 135 | else 136 | apt_get_update_if_needed 137 | fi 138 | 139 | # Install libssl1.1 if available 140 | if [[ ! -z $(apt-cache --names-only search ^libssl1.1$) ]]; then 141 | package_list="${package_list} libssl1.1" 142 | fi 143 | 144 | # Install appropriate version of libssl1.0.x if available 145 | libssl_package=$(dpkg-query -f '${db:Status-Abbrev}\t${binary:Package}\n' -W 'libssl1\.0\.?' 2>&1 || echo '') 146 | if [ "$(echo "$LIlibssl_packageBSSL" | grep -o 'libssl1\.0\.[0-9]:' | uniq | sort | wc -l)" -eq 0 ]; then 147 | if [[ ! -z $(apt-cache --names-only search ^libssl1.0.2$) ]]; then 148 | # Debian 9 149 | package_list="${package_list} libssl1.0.2" 150 | elif [[ ! -z $(apt-cache --names-only search ^libssl1.0.0$) ]]; then 151 | # Ubuntu 18.04, 16.04, earlier 152 | package_list="${package_list} libssl1.0.0" 153 | fi 154 | fi 155 | 156 | echo "Packages to verify are installed: ${package_list}" 157 | apt-get -y install --no-install-recommends ${package_list} 2> >( grep -v 'debconf: delaying package configuration, since apt-utils is not installed' >&2 ) 158 | 159 | # Install git if not already installed (may be more recent than distro version) 160 | if ! type git > /dev/null 2>&1; then 161 | apt-get -y install --no-install-recommends git 162 | fi 163 | 164 | PACKAGES_ALREADY_INSTALLED="true" 165 | fi 166 | 167 | # Get to latest versions of all packages 168 | if [ "${UPGRADE_PACKAGES}" = "true" ]; then 169 | apt_get_update_if_needed 170 | apt-get -y upgrade --no-install-recommends 171 | apt-get autoremove -y 172 | fi 173 | 174 | # Ensure at least the en_US.UTF-8 UTF-8 locale is available. 175 | # Common need for both applications and things like the agnoster ZSH theme. 176 | if [ "${LOCALE_ALREADY_SET}" != "true" ] && ! grep -o -E '^\s*en_US.UTF-8\s+UTF-8' /etc/locale.gen > /dev/null; then 177 | echo "en_US.UTF-8 UTF-8" >> /etc/locale.gen 178 | locale-gen 179 | LOCALE_ALREADY_SET="true" 180 | fi 181 | 182 | # Create or update a non-root user to match UID/GID. 183 | group_name="${USERNAME}" 184 | if id -u ${USERNAME} > /dev/null 2>&1; then 185 | # User exists, update if needed 186 | if [ "${USER_GID}" != "automatic" ] && [ "$USER_GID" != "$(id -g $USERNAME)" ]; then 187 | group_name="$(id -gn $USERNAME)" 188 | groupmod --gid $USER_GID ${group_name} 189 | usermod --gid $USER_GID $USERNAME 190 | fi 191 | if [ "${USER_UID}" != "automatic" ] && [ "$USER_UID" != "$(id -u $USERNAME)" ]; then 192 | usermod --uid $USER_UID $USERNAME 193 | fi 194 | else 195 | # Create user 196 | if [ "${USER_GID}" = "automatic" ]; then 197 | groupadd $USERNAME 198 | else 199 | groupadd --gid $USER_GID $USERNAME 200 | fi 201 | if [ "${USER_UID}" = "automatic" ]; then 202 | useradd -s /bin/bash --gid $USERNAME -m $USERNAME 203 | else 204 | useradd -s /bin/bash --uid $USER_UID --gid $USERNAME -m $USERNAME 205 | fi 206 | fi 207 | 208 | # Add add sudo support for non-root user 209 | if [ "${USERNAME}" != "root" ] && [ "${EXISTING_NON_ROOT_USER}" != "${USERNAME}" ]; then 210 | echo $USERNAME ALL=\(root\) NOPASSWD:ALL > /etc/sudoers.d/$USERNAME 211 | chmod 0440 /etc/sudoers.d/$USERNAME 212 | EXISTING_NON_ROOT_USER="${USERNAME}" 213 | fi 214 | 215 | # ** Shell customization section ** 216 | if [ "${USERNAME}" = "root" ]; then 217 | user_rc_path="/root" 218 | else 219 | user_rc_path="/home/${USERNAME}" 220 | fi 221 | 222 | # Restore user .bashrc defaults from skeleton file if it doesn't exist or is empty 223 | if [ ! -f "${user_rc_path}/.bashrc" ] || [ ! -s "${user_rc_path}/.bashrc" ] ; then 224 | cp /etc/skel/.bashrc "${user_rc_path}/.bashrc" 225 | fi 226 | 227 | # Restore user .profile defaults from skeleton file if it doesn't exist or is empty 228 | if [ ! -f "${user_rc_path}/.profile" ] || [ ! -s "${user_rc_path}/.profile" ] ; then 229 | cp /etc/skel/.profile "${user_rc_path}/.profile" 230 | fi 231 | 232 | # .bashrc/.zshrc snippet 233 | rc_snippet="$(cat << 'EOF' 234 | 235 | if [ -z "${USER}" ]; then export USER=$(whoami); fi 236 | if [[ "${PATH}" != *"$HOME/.local/bin"* ]]; then export PATH="${PATH}:$HOME/.local/bin"; fi 237 | 238 | # Display optional first run image specific notice if configured and terminal is interactive 239 | if [ -t 1 ] && [[ "${TERM_PROGRAM}" = "vscode" || "${TERM_PROGRAM}" = "codespaces" ]] && [ ! -f "$HOME/.config/vscode-dev-containers/first-run-notice-already-displayed" ]; then 240 | if [ -f "/usr/local/etc/vscode-dev-containers/first-run-notice.txt" ]; then 241 | cat "/usr/local/etc/vscode-dev-containers/first-run-notice.txt" 242 | elif [ -f "/workspaces/.codespaces/shared/first-run-notice.txt" ]; then 243 | cat "/workspaces/.codespaces/shared/first-run-notice.txt" 244 | fi 245 | mkdir -p "$HOME/.config/vscode-dev-containers" 246 | # Mark first run notice as displayed after 10s to avoid problems with fast terminal refreshes hiding it 247 | ((sleep 10s; touch "$HOME/.config/vscode-dev-containers/first-run-notice-already-displayed") &) 248 | fi 249 | 250 | # Set the default git editor if not already set 251 | if [ -z "$(git config --get core.editor)" ] && [ -z "${GIT_EDITOR}" ]; then 252 | if [ "${TERM_PROGRAM}" = "vscode" ]; then 253 | if [[ -n $(command -v code-insiders) && -z $(command -v code) ]]; then 254 | export GIT_EDITOR="code-insiders --wait" 255 | else 256 | export GIT_EDITOR="code --wait" 257 | fi 258 | fi 259 | fi 260 | 261 | EOF 262 | )" 263 | 264 | # code shim, it fallbacks to code-insiders if code is not available 265 | cat << 'EOF' > /usr/local/bin/code 266 | #!/bin/sh 267 | 268 | get_in_path_except_current() { 269 | which -a "$1" | grep -A1 "$0" | grep -v "$0" 270 | } 271 | 272 | code="$(get_in_path_except_current code)" 273 | 274 | if [ -n "$code" ]; then 275 | exec "$code" "$@" 276 | elif [ "$(command -v code-insiders)" ]; then 277 | exec code-insiders "$@" 278 | else 279 | echo "code or code-insiders is not installed" >&2 280 | exit 127 281 | fi 282 | EOF 283 | chmod +x /usr/local/bin/code 284 | 285 | # systemctl shim - tells people to use 'service' if systemd is not running 286 | cat << 'EOF' > /usr/local/bin/systemctl 287 | #!/bin/sh 288 | set -e 289 | if [ -d "/run/systemd/system" ]; then 290 | exec /bin/systemctl/systemctl "$@" 291 | else 292 | echo '\n"systemd" is not running in this container due to its overhead.\nUse the "service" command to start services intead. e.g.: \n\nservice --status-all' 293 | fi 294 | EOF 295 | chmod +x /usr/local/bin/systemctl 296 | 297 | # Codespaces bash and OMZ themes - partly inspired by https://github.com/ohmyzsh/ohmyzsh/blob/master/themes/robbyrussell.zsh-theme 298 | codespaces_bash="$(cat \ 299 | <<'EOF' 300 | 301 | # Codespaces bash prompt theme 302 | __bash_prompt() { 303 | local userpart='`export XIT=$? \ 304 | && [ ! -z "${GITHUB_USER}" ] && echo -n "\[\033[0;32m\]@${GITHUB_USER} " || echo -n "\[\033[0;32m\]\u " \ 305 | && [ "$XIT" -ne "0" ] && echo -n "\[\033[1;31m\]➜" || echo -n "\[\033[0m\]➜"`' 306 | local gitbranch='`\ 307 | if [ "$(git config --get codespaces-theme.hide-status 2>/dev/null)" != 1 ]; then \ 308 | export BRANCH=$(git symbolic-ref --short HEAD 2>/dev/null || git rev-parse --short HEAD 2>/dev/null); \ 309 | if [ "${BRANCH}" != "" ]; then \ 310 | echo -n "\[\033[0;36m\](\[\033[1;31m\]${BRANCH}" \ 311 | && if git ls-files --error-unmatch -m --directory --no-empty-directory -o --exclude-standard ":/*" > /dev/null 2>&1; then \ 312 | echo -n " \[\033[1;33m\]✗"; \ 313 | fi \ 314 | && echo -n "\[\033[0;36m\]) "; \ 315 | fi; \ 316 | fi`' 317 | local lightblue='\[\033[1;34m\]' 318 | local removecolor='\[\033[0m\]' 319 | PS1="${userpart} ${lightblue}\w ${gitbranch}${removecolor}\$ " 320 | unset -f __bash_prompt 321 | } 322 | __bash_prompt 323 | 324 | EOF 325 | )" 326 | 327 | codespaces_zsh="$(cat \ 328 | <<'EOF' 329 | # Codespaces zsh prompt theme 330 | __zsh_prompt() { 331 | local prompt_username 332 | if [ ! -z "${GITHUB_USER}" ]; then 333 | prompt_username="@${GITHUB_USER}" 334 | else 335 | prompt_username="%n" 336 | fi 337 | PROMPT="%{$fg[green]%}${prompt_username} %(?:%{$reset_color%}➜ :%{$fg_bold[red]%}➜ )" # User/exit code arrow 338 | PROMPT+='%{$fg_bold[blue]%}%(5~|%-1~/…/%3~|%4~)%{$reset_color%} ' # cwd 339 | PROMPT+='$([ "$(git config --get codespaces-theme.hide-status 2>/dev/null)" != 1 ] && git_prompt_info)' # Git status 340 | PROMPT+='%{$fg[white]%}$ %{$reset_color%}' 341 | unset -f __zsh_prompt 342 | } 343 | ZSH_THEME_GIT_PROMPT_PREFIX="%{$fg_bold[cyan]%}(%{$fg_bold[red]%}" 344 | ZSH_THEME_GIT_PROMPT_SUFFIX="%{$reset_color%} " 345 | ZSH_THEME_GIT_PROMPT_DIRTY=" %{$fg_bold[yellow]%}✗%{$fg_bold[cyan]%})" 346 | ZSH_THEME_GIT_PROMPT_CLEAN="%{$fg_bold[cyan]%})" 347 | __zsh_prompt 348 | 349 | EOF 350 | )" 351 | 352 | # Add RC snippet and custom bash prompt 353 | if [ "${RC_SNIPPET_ALREADY_ADDED}" != "true" ]; then 354 | echo "${rc_snippet}" >> /etc/bash.bashrc 355 | echo "${codespaces_bash}" >> "${user_rc_path}/.bashrc" 356 | echo 'export PROMPT_DIRTRIM=4' >> "${user_rc_path}/.bashrc" 357 | if [ "${USERNAME}" != "root" ]; then 358 | echo "${codespaces_bash}" >> "/root/.bashrc" 359 | echo 'export PROMPT_DIRTRIM=4' >> "/root/.bashrc" 360 | fi 361 | chown ${USERNAME}:${group_name} "${user_rc_path}/.bashrc" 362 | RC_SNIPPET_ALREADY_ADDED="true" 363 | fi 364 | 365 | # Optionally install and configure zsh and Oh My Zsh! 366 | if [ "${INSTALL_ZSH}" = "true" ]; then 367 | if ! type zsh > /dev/null 2>&1; then 368 | apt_get_update_if_needed 369 | apt-get install -y zsh 370 | fi 371 | if [ "${ZSH_ALREADY_INSTALLED}" != "true" ]; then 372 | echo "${rc_snippet}" >> /etc/zsh/zshrc 373 | ZSH_ALREADY_INSTALLED="true" 374 | fi 375 | 376 | # Adapted, simplified inline Oh My Zsh! install steps that adds, defaults to a codespaces theme. 377 | # See https://github.com/ohmyzsh/ohmyzsh/blob/master/tools/install.sh for official script. 378 | oh_my_install_dir="${user_rc_path}/.oh-my-zsh" 379 | if [ ! -d "${oh_my_install_dir}" ] && [ "${INSTALL_OH_MYS}" = "true" ]; then 380 | template_path="${oh_my_install_dir}/templates/zshrc.zsh-template" 381 | user_rc_file="${user_rc_path}/.zshrc" 382 | umask g-w,o-w 383 | mkdir -p ${oh_my_install_dir} 384 | git clone --depth=1 \ 385 | -c core.eol=lf \ 386 | -c core.autocrlf=false \ 387 | -c fsck.zeroPaddedFilemode=ignore \ 388 | -c fetch.fsck.zeroPaddedFilemode=ignore \ 389 | -c receive.fsck.zeroPaddedFilemode=ignore \ 390 | "https://github.com/ohmyzsh/ohmyzsh" "${oh_my_install_dir}" 2>&1 391 | echo -e "$(cat "${template_path}")\nDISABLE_AUTO_UPDATE=true\nDISABLE_UPDATE_PROMPT=true" > ${user_rc_file} 392 | sed -i -e 's/ZSH_THEME=.*/ZSH_THEME="codespaces"/g' ${user_rc_file} 393 | 394 | mkdir -p ${oh_my_install_dir}/custom/themes 395 | echo "${codespaces_zsh}" > "${oh_my_install_dir}/custom/themes/codespaces.zsh-theme" 396 | # Shrink git while still enabling updates 397 | cd "${oh_my_install_dir}" 398 | git repack -a -d -f --depth=1 --window=1 399 | # Copy to non-root user if one is specified 400 | if [ "${USERNAME}" != "root" ]; then 401 | cp -rf "${user_rc_file}" "${oh_my_install_dir}" /root 402 | chown -R ${USERNAME}:${group_name} "${user_rc_path}" 403 | fi 404 | fi 405 | fi 406 | 407 | # Persist image metadata info, script if meta.env found in same directory 408 | meta_info_script="$(cat << 'EOF' 409 | #!/bin/sh 410 | . /usr/local/etc/vscode-dev-containers/meta.env 411 | 412 | # Minimal output 413 | if [ "$1" = "version" ] || [ "$1" = "image-version" ]; then 414 | echo "${VERSION}" 415 | exit 0 416 | elif [ "$1" = "release" ]; then 417 | echo "${GIT_REPOSITORY_RELEASE}" 418 | exit 0 419 | elif [ "$1" = "content" ] || [ "$1" = "content-url" ] || [ "$1" = "contents" ] || [ "$1" = "contents-url" ]; then 420 | echo "${CONTENTS_URL}" 421 | exit 0 422 | fi 423 | 424 | #Full output 425 | echo 426 | echo "Development container image information" 427 | echo 428 | if [ ! -z "${VERSION}" ]; then echo "- Image version: ${VERSION}"; fi 429 | if [ ! -z "${DEFINITION_ID}" ]; then echo "- Definition ID: ${DEFINITION_ID}"; fi 430 | if [ ! -z "${VARIANT}" ]; then echo "- Variant: ${VARIANT}"; fi 431 | if [ ! -z "${GIT_REPOSITORY}" ]; then echo "- Source code repository: ${GIT_REPOSITORY}"; fi 432 | if [ ! -z "${GIT_REPOSITORY_RELEASE}" ]; then echo "- Source code release/branch: ${GIT_REPOSITORY_RELEASE}"; fi 433 | if [ ! -z "${BUILD_TIMESTAMP}" ]; then echo "- Timestamp: ${BUILD_TIMESTAMP}"; fi 434 | if [ ! -z "${CONTENTS_URL}" ]; then echo && echo "More info: ${CONTENTS_URL}"; fi 435 | echo 436 | EOF 437 | )" 438 | if [ -f "${SCRIPT_DIR}/meta.env" ]; then 439 | mkdir -p /usr/local/etc/vscode-dev-containers/ 440 | cp -f "${SCRIPT_DIR}/meta.env" /usr/local/etc/vscode-dev-containers/meta.env 441 | echo "${meta_info_script}" > /usr/local/bin/devcontainer-info 442 | chmod +x /usr/local/bin/devcontainer-info 443 | fi 444 | 445 | # Write marker file 446 | mkdir -p "$(dirname "${MARKER_FILE}")" 447 | echo -e "\ 448 | PACKAGES_ALREADY_INSTALLED=${PACKAGES_ALREADY_INSTALLED}\n\ 449 | LOCALE_ALREADY_SET=${LOCALE_ALREADY_SET}\n\ 450 | EXISTING_NON_ROOT_USER=${EXISTING_NON_ROOT_USER}\n\ 451 | RC_SNIPPET_ALREADY_ADDED=${RC_SNIPPET_ALREADY_ADDED}\n\ 452 | ZSH_ALREADY_INSTALLED=${ZSH_ALREADY_INSTALLED}" > "${MARKER_FILE}" 453 | 454 | echo "Done!" 455 | -------------------------------------------------------------------------------- /mt/mt/devcontainer/build-script/common-debian.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | #------------------------------------------------------------------------------------------------------------- 3 | # Copyright (c) Microsoft Corporation. All rights reserved. 4 | # Licensed under the MIT License. See https://go.microsoft.com/fwlink/?linkid=2090316 for license information. 5 | #------------------------------------------------------------------------------------------------------------- 6 | # 7 | # Docs: https://github.com/microsoft/vscode-dev-containers/blob/main/script-library/docs/common.md 8 | # Maintainer: The VS Code and Codespaces Teams 9 | # 10 | # Syntax: ./common-debian.sh [install zsh flag] [username] [user UID] [user GID] [upgrade packages flag] [install Oh My Zsh! flag] [Add non-free packages] 11 | 12 | set -e 13 | 14 | INSTALL_ZSH=${1:-"true"} 15 | USERNAME=${2:-"automatic"} 16 | USER_UID=${3:-"automatic"} 17 | USER_GID=${4:-"automatic"} 18 | UPGRADE_PACKAGES=${5:-"true"} 19 | INSTALL_OH_MYS=${6:-"true"} 20 | ADD_NON_FREE_PACKAGES=${7:-"false"} 21 | SCRIPT_DIR="$(cd $(dirname "${BASH_SOURCE[0]}") && pwd)" 22 | MARKER_FILE="/usr/local/etc/vscode-dev-containers/common" 23 | 24 | if [ "$(id -u)" -ne 0 ]; then 25 | echo -e 'Script must be run as root. Use sudo, su, or add "USER root" to your Dockerfile before running this script.' 26 | exit 1 27 | fi 28 | 29 | # Ensure that login shells get the correct path if the user updated the PATH using ENV. 30 | rm -f /etc/profile.d/00-restore-env.sh 31 | echo "export PATH=${PATH//$(sh -lc 'echo $PATH')/\$PATH}" > /etc/profile.d/00-restore-env.sh 32 | chmod +x /etc/profile.d/00-restore-env.sh 33 | 34 | # If in automatic mode, determine if a user already exists, if not use vscode 35 | if [ "${USERNAME}" = "auto" ] || [ "${USERNAME}" = "automatic" ]; then 36 | USERNAME="" 37 | POSSIBLE_USERS=("vscode" "node" "codespace" "$(awk -v val=1000 -F ":" '$3==val{print $1}' /etc/passwd)") 38 | for CURRENT_USER in ${POSSIBLE_USERS[@]}; do 39 | if id -u ${CURRENT_USER} > /dev/null 2>&1; then 40 | USERNAME=${CURRENT_USER} 41 | break 42 | fi 43 | done 44 | if [ "${USERNAME}" = "" ]; then 45 | USERNAME=vscode 46 | fi 47 | elif [ "${USERNAME}" = "none" ]; then 48 | USERNAME=root 49 | USER_UID=0 50 | USER_GID=0 51 | fi 52 | 53 | # Load markers to see which steps have already run 54 | if [ -f "${MARKER_FILE}" ]; then 55 | echo "Marker file found:" 56 | cat "${MARKER_FILE}" 57 | source "${MARKER_FILE}" 58 | fi 59 | 60 | # Ensure apt is in non-interactive to avoid prompts 61 | export DEBIAN_FRONTEND=noninteractive 62 | 63 | # Function to call apt-get if needed 64 | apt_get_update_if_needed() 65 | { 66 | if [ ! -d "/var/lib/apt/lists" ] || [ "$(ls /var/lib/apt/lists/ | wc -l)" = "0" ]; then 67 | echo "Running apt-get update..." 68 | apt-get update 69 | else 70 | echo "Skipping apt-get update." 71 | fi 72 | } 73 | 74 | # Run install apt-utils to avoid debconf warning then verify presence of other common developer tools and dependencies 75 | if [ "${PACKAGES_ALREADY_INSTALLED}" != "true" ]; then 76 | 77 | package_list="apt-utils \ 78 | openssh-client \ 79 | gnupg2 \ 80 | dirmngr \ 81 | iproute2 \ 82 | procps \ 83 | lsof \ 84 | htop \ 85 | net-tools \ 86 | psmisc \ 87 | curl \ 88 | wget \ 89 | rsync \ 90 | ca-certificates \ 91 | unzip \ 92 | zip \ 93 | nano \ 94 | vim-tiny \ 95 | less \ 96 | jq \ 97 | lsb-release \ 98 | apt-transport-https \ 99 | dialog \ 100 | libc6 \ 101 | libgcc1 \ 102 | libkrb5-3 \ 103 | libgssapi-krb5-2 \ 104 | libicu[0-9][0-9] \ 105 | liblttng-ust0 \ 106 | libstdc++6 \ 107 | zlib1g \ 108 | locales \ 109 | sudo \ 110 | ncdu \ 111 | man-db \ 112 | strace \ 113 | manpages \ 114 | manpages-dev \ 115 | init-system-helpers" 116 | 117 | # Needed for adding manpages-posix and manpages-posix-dev which are non-free packages in Debian 118 | if [ "${ADD_NON_FREE_PACKAGES}" = "true" ]; then 119 | # Bring in variables from /etc/os-release like VERSION_CODENAME 120 | . /etc/os-release 121 | sed -i -E "s/deb http:\/\/(deb|httpredir)\.debian\.org\/debian ${VERSION_CODENAME} main/deb http:\/\/\1\.debian\.org\/debian ${VERSION_CODENAME} main contrib non-free/" /etc/apt/sources.list 122 | sed -i -E "s/deb-src http:\/\/(deb|httredir)\.debian\.org\/debian ${VERSION_CODENAME} main/deb http:\/\/\1\.debian\.org\/debian ${VERSION_CODENAME} main contrib non-free/" /etc/apt/sources.list 123 | sed -i -E "s/deb http:\/\/(deb|httpredir)\.debian\.org\/debian ${VERSION_CODENAME}-updates main/deb http:\/\/\1\.debian\.org\/debian ${VERSION_CODENAME}-updates main contrib non-free/" /etc/apt/sources.list 124 | sed -i -E "s/deb-src http:\/\/(deb|httpredir)\.debian\.org\/debian ${VERSION_CODENAME}-updates main/deb http:\/\/\1\.debian\.org\/debian ${VERSION_CODENAME}-updates main contrib non-free/" /etc/apt/sources.list 125 | sed -i "s/deb http:\/\/security\.debian\.org\/debian-security ${VERSION_CODENAME}\/updates main/deb http:\/\/security\.debian\.org\/debian-security ${VERSION_CODENAME}\/updates main contrib non-free/" /etc/apt/sources.list 126 | sed -i "s/deb-src http:\/\/security\.debian\.org\/debian-security ${VERSION_CODENAME}\/updates main/deb http:\/\/security\.debian\.org\/debian-security ${VERSION_CODENAME}\/updates main contrib non-free/" /etc/apt/sources.list 127 | sed -i "s/deb http:\/\/deb\.debian\.org\/debian ${VERSION_CODENAME}-backports main/deb http:\/\/deb\.debian\.org\/debian ${VERSION_CODENAME}-backports main contrib non-free/" /etc/apt/sources.list 128 | sed -i "s/deb-src http:\/\/deb\.debian\.org\/debian ${VERSION_CODENAME}-backports main/deb http:\/\/deb\.debian\.org\/debian ${VERSION_CODENAME}-backports main contrib non-free/" /etc/apt/sources.list 129 | # Handle bullseye location for security https://www.debian.org/releases/bullseye/amd64/release-notes/ch-information.en.html 130 | sed -i "s/deb http:\/\/security\.debian\.org\/debian-security ${VERSION_CODENAME}-security main/deb http:\/\/security\.debian\.org\/debian-security ${VERSION_CODENAME}-security main contrib non-free/" /etc/apt/sources.list 131 | sed -i "s/deb-src http:\/\/security\.debian\.org\/debian-security ${VERSION_CODENAME}-security main/deb http:\/\/security\.debian\.org\/debian-security ${VERSION_CODENAME}-security main contrib non-free/" /etc/apt/sources.list 132 | echo "Running apt-get update..." 133 | apt-get update 134 | package_list="${package_list} manpages-posix manpages-posix-dev" 135 | else 136 | apt_get_update_if_needed 137 | fi 138 | 139 | # Install libssl1.1 if available 140 | if [[ ! -z $(apt-cache --names-only search ^libssl1.1$) ]]; then 141 | package_list="${package_list} libssl1.1" 142 | fi 143 | 144 | # Install appropriate version of libssl1.0.x if available 145 | libssl_package=$(dpkg-query -f '${db:Status-Abbrev}\t${binary:Package}\n' -W 'libssl1\.0\.?' 2>&1 || echo '') 146 | if [ "$(echo "$LIlibssl_packageBSSL" | grep -o 'libssl1\.0\.[0-9]:' | uniq | sort | wc -l)" -eq 0 ]; then 147 | if [[ ! -z $(apt-cache --names-only search ^libssl1.0.2$) ]]; then 148 | # Debian 9 149 | package_list="${package_list} libssl1.0.2" 150 | elif [[ ! -z $(apt-cache --names-only search ^libssl1.0.0$) ]]; then 151 | # Ubuntu 18.04, 16.04, earlier 152 | package_list="${package_list} libssl1.0.0" 153 | fi 154 | fi 155 | 156 | echo "Packages to verify are installed: ${package_list}" 157 | apt-get -y install --no-install-recommends ${package_list} 2> >( grep -v 'debconf: delaying package configuration, since apt-utils is not installed' >&2 ) 158 | 159 | # Install git if not already installed (may be more recent than distro version) 160 | if ! type git > /dev/null 2>&1; then 161 | apt-get -y install --no-install-recommends git 162 | fi 163 | 164 | PACKAGES_ALREADY_INSTALLED="true" 165 | fi 166 | 167 | # Get to latest versions of all packages 168 | if [ "${UPGRADE_PACKAGES}" = "true" ]; then 169 | apt_get_update_if_needed 170 | apt-get -y upgrade --no-install-recommends 171 | apt-get autoremove -y 172 | fi 173 | 174 | # Ensure at least the en_US.UTF-8 UTF-8 locale is available. 175 | # Common need for both applications and things like the agnoster ZSH theme. 176 | if [ "${LOCALE_ALREADY_SET}" != "true" ] && ! grep -o -E '^\s*en_US.UTF-8\s+UTF-8' /etc/locale.gen > /dev/null; then 177 | echo "en_US.UTF-8 UTF-8" >> /etc/locale.gen 178 | locale-gen 179 | LOCALE_ALREADY_SET="true" 180 | fi 181 | 182 | # Create or update a non-root user to match UID/GID. 183 | group_name="${USERNAME}" 184 | if id -u ${USERNAME} > /dev/null 2>&1; then 185 | # User exists, update if needed 186 | if [ "${USER_GID}" != "automatic" ] && [ "$USER_GID" != "$(id -g $USERNAME)" ]; then 187 | group_name="$(id -gn $USERNAME)" 188 | groupmod --gid $USER_GID ${group_name} 189 | usermod --gid $USER_GID $USERNAME 190 | fi 191 | if [ "${USER_UID}" != "automatic" ] && [ "$USER_UID" != "$(id -u $USERNAME)" ]; then 192 | usermod --uid $USER_UID $USERNAME 193 | fi 194 | else 195 | # Create user 196 | if [ "${USER_GID}" = "automatic" ]; then 197 | groupadd $USERNAME 198 | else 199 | groupadd --gid $USER_GID $USERNAME 200 | fi 201 | if [ "${USER_UID}" = "automatic" ]; then 202 | useradd -s /bin/bash --gid $USERNAME -m $USERNAME 203 | else 204 | useradd -s /bin/bash --uid $USER_UID --gid $USERNAME -m $USERNAME 205 | fi 206 | fi 207 | 208 | # Add add sudo support for non-root user 209 | if [ "${USERNAME}" != "root" ] && [ "${EXISTING_NON_ROOT_USER}" != "${USERNAME}" ]; then 210 | echo $USERNAME ALL=\(root\) NOPASSWD:ALL > /etc/sudoers.d/$USERNAME 211 | chmod 0440 /etc/sudoers.d/$USERNAME 212 | EXISTING_NON_ROOT_USER="${USERNAME}" 213 | fi 214 | 215 | # ** Shell customization section ** 216 | if [ "${USERNAME}" = "root" ]; then 217 | user_rc_path="/root" 218 | else 219 | user_rc_path="/home/${USERNAME}" 220 | fi 221 | 222 | # Restore user .bashrc defaults from skeleton file if it doesn't exist or is empty 223 | if [ ! -f "${user_rc_path}/.bashrc" ] || [ ! -s "${user_rc_path}/.bashrc" ] ; then 224 | cp /etc/skel/.bashrc "${user_rc_path}/.bashrc" 225 | fi 226 | 227 | # Restore user .profile defaults from skeleton file if it doesn't exist or is empty 228 | if [ ! -f "${user_rc_path}/.profile" ] || [ ! -s "${user_rc_path}/.profile" ] ; then 229 | cp /etc/skel/.profile "${user_rc_path}/.profile" 230 | fi 231 | 232 | # .bashrc/.zshrc snippet 233 | rc_snippet="$(cat << 'EOF' 234 | 235 | if [ -z "${USER}" ]; then export USER=$(whoami); fi 236 | if [[ "${PATH}" != *"$HOME/.local/bin"* ]]; then export PATH="${PATH}:$HOME/.local/bin"; fi 237 | 238 | # Display optional first run image specific notice if configured and terminal is interactive 239 | if [ -t 1 ] && [[ "${TERM_PROGRAM}" = "vscode" || "${TERM_PROGRAM}" = "codespaces" ]] && [ ! -f "$HOME/.config/vscode-dev-containers/first-run-notice-already-displayed" ]; then 240 | if [ -f "/usr/local/etc/vscode-dev-containers/first-run-notice.txt" ]; then 241 | cat "/usr/local/etc/vscode-dev-containers/first-run-notice.txt" 242 | elif [ -f "/workspaces/.codespaces/shared/first-run-notice.txt" ]; then 243 | cat "/workspaces/.codespaces/shared/first-run-notice.txt" 244 | fi 245 | mkdir -p "$HOME/.config/vscode-dev-containers" 246 | # Mark first run notice as displayed after 10s to avoid problems with fast terminal refreshes hiding it 247 | ((sleep 10s; touch "$HOME/.config/vscode-dev-containers/first-run-notice-already-displayed") &) 248 | fi 249 | 250 | # Set the default git editor if not already set 251 | if [ -z "$(git config --get core.editor)" ] && [ -z "${GIT_EDITOR}" ]; then 252 | if [ "${TERM_PROGRAM}" = "vscode" ]; then 253 | if [[ -n $(command -v code-insiders) && -z $(command -v code) ]]; then 254 | export GIT_EDITOR="code-insiders --wait" 255 | else 256 | export GIT_EDITOR="code --wait" 257 | fi 258 | fi 259 | fi 260 | 261 | EOF 262 | )" 263 | 264 | # code shim, it fallbacks to code-insiders if code is not available 265 | cat << 'EOF' > /usr/local/bin/code 266 | #!/bin/sh 267 | 268 | get_in_path_except_current() { 269 | which -a "$1" | grep -A1 "$0" | grep -v "$0" 270 | } 271 | 272 | code="$(get_in_path_except_current code)" 273 | 274 | if [ -n "$code" ]; then 275 | exec "$code" "$@" 276 | elif [ "$(command -v code-insiders)" ]; then 277 | exec code-insiders "$@" 278 | else 279 | echo "code or code-insiders is not installed" >&2 280 | exit 127 281 | fi 282 | EOF 283 | chmod +x /usr/local/bin/code 284 | 285 | # systemctl shim - tells people to use 'service' if systemd is not running 286 | cat << 'EOF' > /usr/local/bin/systemctl 287 | #!/bin/sh 288 | set -e 289 | if [ -d "/run/systemd/system" ]; then 290 | exec /bin/systemctl/systemctl "$@" 291 | else 292 | echo '\n"systemd" is not running in this container due to its overhead.\nUse the "service" command to start services intead. e.g.: \n\nservice --status-all' 293 | fi 294 | EOF 295 | chmod +x /usr/local/bin/systemctl 296 | 297 | # Codespaces bash and OMZ themes - partly inspired by https://github.com/ohmyzsh/ohmyzsh/blob/master/themes/robbyrussell.zsh-theme 298 | codespaces_bash="$(cat \ 299 | <<'EOF' 300 | 301 | # Codespaces bash prompt theme 302 | __bash_prompt() { 303 | local userpart='`export XIT=$? \ 304 | && [ ! -z "${GITHUB_USER}" ] && echo -n "\[\033[0;32m\]@${GITHUB_USER} " || echo -n "\[\033[0;32m\]\u " \ 305 | && [ "$XIT" -ne "0" ] && echo -n "\[\033[1;31m\]➜" || echo -n "\[\033[0m\]➜"`' 306 | local gitbranch='`\ 307 | if [ "$(git config --get codespaces-theme.hide-status 2>/dev/null)" != 1 ]; then \ 308 | export BRANCH=$(git symbolic-ref --short HEAD 2>/dev/null || git rev-parse --short HEAD 2>/dev/null); \ 309 | if [ "${BRANCH}" != "" ]; then \ 310 | echo -n "\[\033[0;36m\](\[\033[1;31m\]${BRANCH}" \ 311 | && if git ls-files --error-unmatch -m --directory --no-empty-directory -o --exclude-standard ":/*" > /dev/null 2>&1; then \ 312 | echo -n " \[\033[1;33m\]✗"; \ 313 | fi \ 314 | && echo -n "\[\033[0;36m\]) "; \ 315 | fi; \ 316 | fi`' 317 | local lightblue='\[\033[1;34m\]' 318 | local removecolor='\[\033[0m\]' 319 | PS1="${userpart} ${lightblue}\w ${gitbranch}${removecolor}\$ " 320 | unset -f __bash_prompt 321 | } 322 | __bash_prompt 323 | 324 | EOF 325 | )" 326 | 327 | codespaces_zsh="$(cat \ 328 | <<'EOF' 329 | # Codespaces zsh prompt theme 330 | __zsh_prompt() { 331 | local prompt_username 332 | if [ ! -z "${GITHUB_USER}" ]; then 333 | prompt_username="@${GITHUB_USER}" 334 | else 335 | prompt_username="%n" 336 | fi 337 | PROMPT="%{$fg[green]%}${prompt_username} %(?:%{$reset_color%}➜ :%{$fg_bold[red]%}➜ )" # User/exit code arrow 338 | PROMPT+='%{$fg_bold[blue]%}%(5~|%-1~/…/%3~|%4~)%{$reset_color%} ' # cwd 339 | PROMPT+='$([ "$(git config --get codespaces-theme.hide-status 2>/dev/null)" != 1 ] && git_prompt_info)' # Git status 340 | PROMPT+='%{$fg[white]%}$ %{$reset_color%}' 341 | unset -f __zsh_prompt 342 | } 343 | ZSH_THEME_GIT_PROMPT_PREFIX="%{$fg_bold[cyan]%}(%{$fg_bold[red]%}" 344 | ZSH_THEME_GIT_PROMPT_SUFFIX="%{$reset_color%} " 345 | ZSH_THEME_GIT_PROMPT_DIRTY=" %{$fg_bold[yellow]%}✗%{$fg_bold[cyan]%})" 346 | ZSH_THEME_GIT_PROMPT_CLEAN="%{$fg_bold[cyan]%})" 347 | __zsh_prompt 348 | 349 | EOF 350 | )" 351 | 352 | # Add RC snippet and custom bash prompt 353 | if [ "${RC_SNIPPET_ALREADY_ADDED}" != "true" ]; then 354 | echo "${rc_snippet}" >> /etc/bash.bashrc 355 | echo "${codespaces_bash}" >> "${user_rc_path}/.bashrc" 356 | echo 'export PROMPT_DIRTRIM=4' >> "${user_rc_path}/.bashrc" 357 | if [ "${USERNAME}" != "root" ]; then 358 | echo "${codespaces_bash}" >> "/root/.bashrc" 359 | echo 'export PROMPT_DIRTRIM=4' >> "/root/.bashrc" 360 | fi 361 | chown ${USERNAME}:${group_name} "${user_rc_path}/.bashrc" 362 | RC_SNIPPET_ALREADY_ADDED="true" 363 | fi 364 | 365 | # Optionally install and configure zsh and Oh My Zsh! 366 | if [ "${INSTALL_ZSH}" = "true" ]; then 367 | if ! type zsh > /dev/null 2>&1; then 368 | apt_get_update_if_needed 369 | apt-get install -y zsh 370 | fi 371 | if [ "${ZSH_ALREADY_INSTALLED}" != "true" ]; then 372 | echo "${rc_snippet}" >> /etc/zsh/zshrc 373 | ZSH_ALREADY_INSTALLED="true" 374 | fi 375 | 376 | # Adapted, simplified inline Oh My Zsh! install steps that adds, defaults to a codespaces theme. 377 | # See https://github.com/ohmyzsh/ohmyzsh/blob/master/tools/install.sh for official script. 378 | oh_my_install_dir="${user_rc_path}/.oh-my-zsh" 379 | if [ ! -d "${oh_my_install_dir}" ] && [ "${INSTALL_OH_MYS}" = "true" ]; then 380 | template_path="${oh_my_install_dir}/templates/zshrc.zsh-template" 381 | user_rc_file="${user_rc_path}/.zshrc" 382 | umask g-w,o-w 383 | mkdir -p ${oh_my_install_dir} 384 | git clone --depth=1 \ 385 | -c core.eol=lf \ 386 | -c core.autocrlf=false \ 387 | -c fsck.zeroPaddedFilemode=ignore \ 388 | -c fetch.fsck.zeroPaddedFilemode=ignore \ 389 | -c receive.fsck.zeroPaddedFilemode=ignore \ 390 | "https://github.com/ohmyzsh/ohmyzsh" "${oh_my_install_dir}" 2>&1 391 | echo -e "$(cat "${template_path}")\nDISABLE_AUTO_UPDATE=true\nDISABLE_UPDATE_PROMPT=true" > ${user_rc_file} 392 | sed -i -e 's/ZSH_THEME=.*/ZSH_THEME="codespaces"/g' ${user_rc_file} 393 | 394 | mkdir -p ${oh_my_install_dir}/custom/themes 395 | echo "${codespaces_zsh}" > "${oh_my_install_dir}/custom/themes/codespaces.zsh-theme" 396 | # Shrink git while still enabling updates 397 | cd "${oh_my_install_dir}" 398 | git repack -a -d -f --depth=1 --window=1 399 | # Copy to non-root user if one is specified 400 | if [ "${USERNAME}" != "root" ]; then 401 | cp -rf "${user_rc_file}" "${oh_my_install_dir}" /root 402 | chown -R ${USERNAME}:${group_name} "${user_rc_path}" 403 | fi 404 | fi 405 | fi 406 | 407 | # Persist image metadata info, script if meta.env found in same directory 408 | meta_info_script="$(cat << 'EOF' 409 | #!/bin/sh 410 | . /usr/local/etc/vscode-dev-containers/meta.env 411 | 412 | # Minimal output 413 | if [ "$1" = "version" ] || [ "$1" = "image-version" ]; then 414 | echo "${VERSION}" 415 | exit 0 416 | elif [ "$1" = "release" ]; then 417 | echo "${GIT_REPOSITORY_RELEASE}" 418 | exit 0 419 | elif [ "$1" = "content" ] || [ "$1" = "content-url" ] || [ "$1" = "contents" ] || [ "$1" = "contents-url" ]; then 420 | echo "${CONTENTS_URL}" 421 | exit 0 422 | fi 423 | 424 | #Full output 425 | echo 426 | echo "Development container image information" 427 | echo 428 | if [ ! -z "${VERSION}" ]; then echo "- Image version: ${VERSION}"; fi 429 | if [ ! -z "${DEFINITION_ID}" ]; then echo "- Definition ID: ${DEFINITION_ID}"; fi 430 | if [ ! -z "${VARIANT}" ]; then echo "- Variant: ${VARIANT}"; fi 431 | if [ ! -z "${GIT_REPOSITORY}" ]; then echo "- Source code repository: ${GIT_REPOSITORY}"; fi 432 | if [ ! -z "${GIT_REPOSITORY_RELEASE}" ]; then echo "- Source code release/branch: ${GIT_REPOSITORY_RELEASE}"; fi 433 | if [ ! -z "${BUILD_TIMESTAMP}" ]; then echo "- Timestamp: ${BUILD_TIMESTAMP}"; fi 434 | if [ ! -z "${CONTENTS_URL}" ]; then echo && echo "More info: ${CONTENTS_URL}"; fi 435 | echo 436 | EOF 437 | )" 438 | if [ -f "${SCRIPT_DIR}/meta.env" ]; then 439 | mkdir -p /usr/local/etc/vscode-dev-containers/ 440 | cp -f "${SCRIPT_DIR}/meta.env" /usr/local/etc/vscode-dev-containers/meta.env 441 | echo "${meta_info_script}" > /usr/local/bin/devcontainer-info 442 | chmod +x /usr/local/bin/devcontainer-info 443 | fi 444 | 445 | # Write marker file 446 | mkdir -p "$(dirname "${MARKER_FILE}")" 447 | echo -e "\ 448 | PACKAGES_ALREADY_INSTALLED=${PACKAGES_ALREADY_INSTALLED}\n\ 449 | LOCALE_ALREADY_SET=${LOCALE_ALREADY_SET}\n\ 450 | EXISTING_NON_ROOT_USER=${EXISTING_NON_ROOT_USER}\n\ 451 | RC_SNIPPET_ALREADY_ADDED=${RC_SNIPPET_ALREADY_ADDED}\n\ 452 | ZSH_ALREADY_INSTALLED=${ZSH_ALREADY_INSTALLED}" > "${MARKER_FILE}" 453 | 454 | echo "Done!" 455 | -------------------------------------------------------------------------------- /bin/local/lib/perl5/YAML/Tiny.pm: -------------------------------------------------------------------------------- 1 | use 5.008001; # sane UTF-8 support 2 | use strict; 3 | use warnings; 4 | package YAML::Tiny; # git description: v1.72-7-g8682f63 5 | # XXX-INGY is 5.8.1 too old/broken for utf8? 6 | # XXX-XDG Lancaster consensus was that it was sufficient until 7 | # proven otherwise 8 | 9 | our $VERSION = '1.73'; 10 | 11 | ##################################################################### 12 | # The YAML::Tiny API. 13 | # 14 | # These are the currently documented API functions/methods and 15 | # exports: 16 | 17 | use Exporter; 18 | our @ISA = qw{ Exporter }; 19 | our @EXPORT = qw{ Load Dump }; 20 | our @EXPORT_OK = qw{ LoadFile DumpFile freeze thaw }; 21 | 22 | ### 23 | # Functional/Export API: 24 | 25 | sub Dump { 26 | return YAML::Tiny->new(@_)->_dump_string; 27 | } 28 | 29 | # XXX-INGY Returning last document seems a bad behavior. 30 | # XXX-XDG I think first would seem more natural, but I don't know 31 | # that it's worth changing now 32 | sub Load { 33 | my $self = YAML::Tiny->_load_string(@_); 34 | if ( wantarray ) { 35 | return @$self; 36 | } else { 37 | # To match YAML.pm, return the last document 38 | return $self->[-1]; 39 | } 40 | } 41 | 42 | # XXX-INGY Do we really need freeze and thaw? 43 | # XXX-XDG I don't think so. I'd support deprecating them. 44 | BEGIN { 45 | *freeze = \&Dump; 46 | *thaw = \&Load; 47 | } 48 | 49 | sub DumpFile { 50 | my $file = shift; 51 | return YAML::Tiny->new(@_)->_dump_file($file); 52 | } 53 | 54 | sub LoadFile { 55 | my $file = shift; 56 | my $self = YAML::Tiny->_load_file($file); 57 | if ( wantarray ) { 58 | return @$self; 59 | } else { 60 | # Return only the last document to match YAML.pm, 61 | return $self->[-1]; 62 | } 63 | } 64 | 65 | 66 | ### 67 | # Object Oriented API: 68 | 69 | # Create an empty YAML::Tiny object 70 | # XXX-INGY Why do we use ARRAY object? 71 | # NOTE: I get it now, but I think it's confusing and not needed. 72 | # Will change it on a branch later, for review. 73 | # 74 | # XXX-XDG I don't support changing it yet. It's a very well-documented 75 | # "API" of YAML::Tiny. I'd support deprecating it, but Adam suggested 76 | # we not change it until YAML.pm's own OO API is established so that 77 | # users only have one API change to digest, not two 78 | sub new { 79 | my $class = shift; 80 | bless [ @_ ], $class; 81 | } 82 | 83 | # XXX-INGY It probably doesn't matter, and it's probably too late to 84 | # change, but 'read/write' are the wrong names. Read and Write 85 | # are actions that take data from storage to memory 86 | # characters/strings. These take the data to/from storage to native 87 | # Perl objects, which the terms dump and load are meant. As long as 88 | # this is a legacy quirk to YAML::Tiny it's ok, but I'd prefer not 89 | # to add new {read,write}_* methods to this API. 90 | 91 | sub read_string { 92 | my $self = shift; 93 | $self->_load_string(@_); 94 | } 95 | 96 | sub write_string { 97 | my $self = shift; 98 | $self->_dump_string(@_); 99 | } 100 | 101 | sub read { 102 | my $self = shift; 103 | $self->_load_file(@_); 104 | } 105 | 106 | sub write { 107 | my $self = shift; 108 | $self->_dump_file(@_); 109 | } 110 | 111 | 112 | 113 | 114 | ##################################################################### 115 | # Constants 116 | 117 | # Printed form of the unprintable characters in the lowest range 118 | # of ASCII characters, listed by ASCII ordinal position. 119 | my @UNPRINTABLE = qw( 120 | 0 x01 x02 x03 x04 x05 x06 a 121 | b t n v f r x0E x0F 122 | x10 x11 x12 x13 x14 x15 x16 x17 123 | x18 x19 x1A e x1C x1D x1E x1F 124 | ); 125 | 126 | # Printable characters for escapes 127 | my %UNESCAPES = ( 128 | 0 => "\x00", z => "\x00", N => "\x85", 129 | a => "\x07", b => "\x08", t => "\x09", 130 | n => "\x0a", v => "\x0b", f => "\x0c", 131 | r => "\x0d", e => "\x1b", '\\' => '\\', 132 | ); 133 | 134 | # XXX-INGY 135 | # I(ngy) need to decide if these values should be quoted in 136 | # YAML::Tiny or not. Probably yes. 137 | 138 | # These 3 values have special meaning when unquoted and using the 139 | # default YAML schema. They need quotes if they are strings. 140 | my %QUOTE = map { $_ => 1 } qw{ 141 | null true false 142 | }; 143 | 144 | # The commented out form is simpler, but overloaded the Perl regex 145 | # engine due to recursion and backtracking problems on strings 146 | # larger than 32,000ish characters. Keep it for reference purposes. 147 | # qr/\"((?:\\.|[^\"])*)\"/ 148 | my $re_capture_double_quoted = qr/\"([^\\"]*(?:\\.[^\\"]*)*)\"/; 149 | my $re_capture_single_quoted = qr/\'([^\']*(?:\'\'[^\']*)*)\'/; 150 | # unquoted re gets trailing space that needs to be stripped 151 | my $re_capture_unquoted_key = qr/([^:]+(?::+\S(?:[^:]*|.*?(?=:)))*)(?=\s*\:(?:\s+|$))/; 152 | my $re_trailing_comment = qr/(?:\s+\#.*)?/; 153 | my $re_key_value_separator = qr/\s*:(?:\s+(?:\#.*)?|$)/; 154 | 155 | 156 | 157 | 158 | 159 | ##################################################################### 160 | # YAML::Tiny Implementation. 161 | # 162 | # These are the private methods that do all the work. They may change 163 | # at any time. 164 | 165 | 166 | ### 167 | # Loader functions: 168 | 169 | # Create an object from a file 170 | sub _load_file { 171 | my $class = ref $_[0] ? ref shift : shift; 172 | 173 | # Check the file 174 | my $file = shift or $class->_error( 'You did not specify a file name' ); 175 | $class->_error( "File '$file' does not exist" ) 176 | unless -e $file; 177 | $class->_error( "'$file' is a directory, not a file" ) 178 | unless -f _; 179 | $class->_error( "Insufficient permissions to read '$file'" ) 180 | unless -r _; 181 | 182 | # Open unbuffered with strict UTF-8 decoding and no translation layers 183 | open( my $fh, "<:unix:encoding(UTF-8)", $file ); 184 | unless ( $fh ) { 185 | $class->_error("Failed to open file '$file': $!"); 186 | } 187 | 188 | # flock if available (or warn if not possible for OS-specific reasons) 189 | if ( _can_flock() ) { 190 | flock( $fh, Fcntl::LOCK_SH() ) 191 | or warn "Couldn't lock '$file' for reading: $!"; 192 | } 193 | 194 | # slurp the contents 195 | my $contents = eval { 196 | use warnings FATAL => 'utf8'; 197 | local $/; 198 | <$fh> 199 | }; 200 | if ( my $err = $@ ) { 201 | $class->_error("Error reading from file '$file': $err"); 202 | } 203 | 204 | # close the file (release the lock) 205 | unless ( close $fh ) { 206 | $class->_error("Failed to close file '$file': $!"); 207 | } 208 | 209 | $class->_load_string( $contents ); 210 | } 211 | 212 | # Create an object from a string 213 | sub _load_string { 214 | my $class = ref $_[0] ? ref shift : shift; 215 | my $self = bless [], $class; 216 | my $string = $_[0]; 217 | eval { 218 | unless ( defined $string ) { 219 | die \"Did not provide a string to load"; 220 | } 221 | 222 | # Check if Perl has it marked as characters, but it's internally 223 | # inconsistent. E.g. maybe latin1 got read on a :utf8 layer 224 | if ( utf8::is_utf8($string) && ! utf8::valid($string) ) { 225 | die \<<'...'; 226 | Read an invalid UTF-8 string (maybe mixed UTF-8 and 8-bit character set). 227 | Did you decode with lax ":utf8" instead of strict ":encoding(UTF-8)"? 228 | ... 229 | } 230 | 231 | # Ensure Unicode character semantics, even for 0x80-0xff 232 | utf8::upgrade($string); 233 | 234 | # Check for and strip any leading UTF-8 BOM 235 | $string =~ s/^\x{FEFF}//; 236 | 237 | # Check for some special cases 238 | return $self unless length $string; 239 | 240 | # Split the file into lines 241 | my @lines = grep { ! /^\s*(?:\#.*)?\z/ } 242 | split /(?:\015{1,2}\012|\015|\012)/, $string; 243 | 244 | # Strip the initial YAML header 245 | @lines and $lines[0] =~ /^\%YAML[: ][\d\.]+.*\z/ and shift @lines; 246 | 247 | # A nibbling parser 248 | my $in_document = 0; 249 | while ( @lines ) { 250 | # Do we have a document header? 251 | if ( $lines[0] =~ /^---\s*(?:(.+)\s*)?\z/ ) { 252 | # Handle scalar documents 253 | shift @lines; 254 | if ( defined $1 and $1 !~ /^(?:\#.+|\%YAML[: ][\d\.]+)\z/ ) { 255 | push @$self, 256 | $self->_load_scalar( "$1", [ undef ], \@lines ); 257 | next; 258 | } 259 | $in_document = 1; 260 | } 261 | 262 | if ( ! @lines or $lines[0] =~ /^(?:---|\.\.\.)/ ) { 263 | # A naked document 264 | push @$self, undef; 265 | while ( @lines and $lines[0] !~ /^---/ ) { 266 | shift @lines; 267 | } 268 | $in_document = 0; 269 | 270 | # XXX The final '-+$' is to look for -- which ends up being an 271 | # error later. 272 | } elsif ( ! $in_document && @$self ) { 273 | # only the first document can be explicit 274 | die \"YAML::Tiny failed to classify the line '$lines[0]'"; 275 | } elsif ( $lines[0] =~ /^\s*\-(?:\s|$|-+$)/ ) { 276 | # An array at the root 277 | my $document = [ ]; 278 | push @$self, $document; 279 | $self->_load_array( $document, [ 0 ], \@lines ); 280 | 281 | } elsif ( $lines[0] =~ /^(\s*)\S/ ) { 282 | # A hash at the root 283 | my $document = { }; 284 | push @$self, $document; 285 | $self->_load_hash( $document, [ length($1) ], \@lines ); 286 | 287 | } else { 288 | # Shouldn't get here. @lines have whitespace-only lines 289 | # stripped, and previous match is a line with any 290 | # non-whitespace. So this clause should only be reachable via 291 | # a perlbug where \s is not symmetric with \S 292 | 293 | # uncoverable statement 294 | die \"YAML::Tiny failed to classify the line '$lines[0]'"; 295 | } 296 | } 297 | }; 298 | my $err = $@; 299 | if ( ref $err eq 'SCALAR' ) { 300 | $self->_error(${$err}); 301 | } elsif ( $err ) { 302 | $self->_error($err); 303 | } 304 | 305 | return $self; 306 | } 307 | 308 | sub _unquote_single { 309 | my ($self, $string) = @_; 310 | return '' unless length $string; 311 | $string =~ s/\'\'/\'/g; 312 | return $string; 313 | } 314 | 315 | sub _unquote_double { 316 | my ($self, $string) = @_; 317 | return '' unless length $string; 318 | $string =~ s/\\"/"/g; 319 | $string =~ 320 | s{\\([Nnever\\fartz0b]|x([0-9a-fA-F]{2}))} 321 | {(length($1)>1)?pack("H2",$2):$UNESCAPES{$1}}gex; 322 | return $string; 323 | } 324 | 325 | # Load a YAML scalar string to the actual Perl scalar 326 | sub _load_scalar { 327 | my ($self, $string, $indent, $lines) = @_; 328 | 329 | # Trim trailing whitespace 330 | $string =~ s/\s*\z//; 331 | 332 | # Explitic null/undef 333 | return undef if $string eq '~'; 334 | 335 | # Single quote 336 | if ( $string =~ /^$re_capture_single_quoted$re_trailing_comment\z/ ) { 337 | return $self->_unquote_single($1); 338 | } 339 | 340 | # Double quote. 341 | if ( $string =~ /^$re_capture_double_quoted$re_trailing_comment\z/ ) { 342 | return $self->_unquote_double($1); 343 | } 344 | 345 | # Special cases 346 | if ( $string =~ /^[\'\"!&]/ ) { 347 | die \"YAML::Tiny does not support a feature in line '$string'"; 348 | } 349 | return {} if $string =~ /^{}(?:\s+\#.*)?\z/; 350 | return [] if $string =~ /^\[\](?:\s+\#.*)?\z/; 351 | 352 | # Regular unquoted string 353 | if ( $string !~ /^[>|]/ ) { 354 | die \"YAML::Tiny found illegal characters in plain scalar: '$string'" 355 | if $string =~ /^(?:-(?:\s|$)|[\@\%\`])/ or 356 | $string =~ /:(?:\s|$)/; 357 | $string =~ s/\s+#.*\z//; 358 | return $string; 359 | } 360 | 361 | # Error 362 | die \"YAML::Tiny failed to find multi-line scalar content" unless @$lines; 363 | 364 | # Check the indent depth 365 | $lines->[0] =~ /^(\s*)/; 366 | $indent->[-1] = length("$1"); 367 | if ( defined $indent->[-2] and $indent->[-1] <= $indent->[-2] ) { 368 | die \"YAML::Tiny found bad indenting in line '$lines->[0]'"; 369 | } 370 | 371 | # Pull the lines 372 | my @multiline = (); 373 | while ( @$lines ) { 374 | $lines->[0] =~ /^(\s*)/; 375 | last unless length($1) >= $indent->[-1]; 376 | push @multiline, substr(shift(@$lines), $indent->[-1]); 377 | } 378 | 379 | my $j = (substr($string, 0, 1) eq '>') ? ' ' : "\n"; 380 | my $t = (substr($string, 1, 1) eq '-') ? '' : "\n"; 381 | return join( $j, @multiline ) . $t; 382 | } 383 | 384 | # Load an array 385 | sub _load_array { 386 | my ($self, $array, $indent, $lines) = @_; 387 | 388 | while ( @$lines ) { 389 | # Check for a new document 390 | if ( $lines->[0] =~ /^(?:---|\.\.\.)/ ) { 391 | while ( @$lines and $lines->[0] !~ /^---/ ) { 392 | shift @$lines; 393 | } 394 | return 1; 395 | } 396 | 397 | # Check the indent level 398 | $lines->[0] =~ /^(\s*)/; 399 | if ( length($1) < $indent->[-1] ) { 400 | return 1; 401 | } elsif ( length($1) > $indent->[-1] ) { 402 | die \"YAML::Tiny found bad indenting in line '$lines->[0]'"; 403 | } 404 | 405 | if ( $lines->[0] =~ /^(\s*\-\s+)[^\'\"]\S*\s*:(?:\s+|$)/ ) { 406 | # Inline nested hash 407 | my $indent2 = length("$1"); 408 | $lines->[0] =~ s/-/ /; 409 | push @$array, { }; 410 | $self->_load_hash( $array->[-1], [ @$indent, $indent2 ], $lines ); 411 | 412 | } elsif ( $lines->[0] =~ /^\s*\-\s*\z/ ) { 413 | shift @$lines; 414 | unless ( @$lines ) { 415 | push @$array, undef; 416 | return 1; 417 | } 418 | if ( $lines->[0] =~ /^(\s*)\-/ ) { 419 | my $indent2 = length("$1"); 420 | if ( $indent->[-1] == $indent2 ) { 421 | # Null array entry 422 | push @$array, undef; 423 | } else { 424 | # Naked indenter 425 | push @$array, [ ]; 426 | $self->_load_array( 427 | $array->[-1], [ @$indent, $indent2 ], $lines 428 | ); 429 | } 430 | 431 | } elsif ( $lines->[0] =~ /^(\s*)\S/ ) { 432 | push @$array, { }; 433 | $self->_load_hash( 434 | $array->[-1], [ @$indent, length("$1") ], $lines 435 | ); 436 | 437 | } else { 438 | die \"YAML::Tiny failed to classify line '$lines->[0]'"; 439 | } 440 | 441 | } elsif ( $lines->[0] =~ /^\s*\-(\s*)(.+?)\s*\z/ ) { 442 | # Array entry with a value 443 | shift @$lines; 444 | push @$array, $self->_load_scalar( 445 | "$2", [ @$indent, undef ], $lines 446 | ); 447 | 448 | } elsif ( defined $indent->[-2] and $indent->[-1] == $indent->[-2] ) { 449 | # This is probably a structure like the following... 450 | # --- 451 | # foo: 452 | # - list 453 | # bar: value 454 | # 455 | # ... so lets return and let the hash parser handle it 456 | return 1; 457 | 458 | } else { 459 | die \"YAML::Tiny failed to classify line '$lines->[0]'"; 460 | } 461 | } 462 | 463 | return 1; 464 | } 465 | 466 | # Load a hash 467 | sub _load_hash { 468 | my ($self, $hash, $indent, $lines) = @_; 469 | 470 | while ( @$lines ) { 471 | # Check for a new document 472 | if ( $lines->[0] =~ /^(?:---|\.\.\.)/ ) { 473 | while ( @$lines and $lines->[0] !~ /^---/ ) { 474 | shift @$lines; 475 | } 476 | return 1; 477 | } 478 | 479 | # Check the indent level 480 | $lines->[0] =~ /^(\s*)/; 481 | if ( length($1) < $indent->[-1] ) { 482 | return 1; 483 | } elsif ( length($1) > $indent->[-1] ) { 484 | die \"YAML::Tiny found bad indenting in line '$lines->[0]'"; 485 | } 486 | 487 | # Find the key 488 | my $key; 489 | 490 | # Quoted keys 491 | if ( $lines->[0] =~ 492 | s/^\s*$re_capture_single_quoted$re_key_value_separator// 493 | ) { 494 | $key = $self->_unquote_single($1); 495 | } 496 | elsif ( $lines->[0] =~ 497 | s/^\s*$re_capture_double_quoted$re_key_value_separator// 498 | ) { 499 | $key = $self->_unquote_double($1); 500 | } 501 | elsif ( $lines->[0] =~ 502 | s/^\s*$re_capture_unquoted_key$re_key_value_separator// 503 | ) { 504 | $key = $1; 505 | $key =~ s/\s+$//; 506 | } 507 | elsif ( $lines->[0] =~ /^\s*\?/ ) { 508 | die \"YAML::Tiny does not support a feature in line '$lines->[0]'"; 509 | } 510 | else { 511 | die \"YAML::Tiny failed to classify line '$lines->[0]'"; 512 | } 513 | 514 | if ( exists $hash->{$key} ) { 515 | warn "YAML::Tiny found a duplicate key '$key' in line '$lines->[0]'"; 516 | } 517 | 518 | # Do we have a value? 519 | if ( length $lines->[0] ) { 520 | # Yes 521 | $hash->{$key} = $self->_load_scalar( 522 | shift(@$lines), [ @$indent, undef ], $lines 523 | ); 524 | } else { 525 | # An indent 526 | shift @$lines; 527 | unless ( @$lines ) { 528 | $hash->{$key} = undef; 529 | return 1; 530 | } 531 | if ( $lines->[0] =~ /^(\s*)-/ ) { 532 | $hash->{$key} = []; 533 | $self->_load_array( 534 | $hash->{$key}, [ @$indent, length($1) ], $lines 535 | ); 536 | } elsif ( $lines->[0] =~ /^(\s*)./ ) { 537 | my $indent2 = length("$1"); 538 | if ( $indent->[-1] >= $indent2 ) { 539 | # Null hash entry 540 | $hash->{$key} = undef; 541 | } else { 542 | $hash->{$key} = {}; 543 | $self->_load_hash( 544 | $hash->{$key}, [ @$indent, length($1) ], $lines 545 | ); 546 | } 547 | } 548 | } 549 | } 550 | 551 | return 1; 552 | } 553 | 554 | 555 | ### 556 | # Dumper functions: 557 | 558 | # Save an object to a file 559 | sub _dump_file { 560 | my $self = shift; 561 | 562 | require Fcntl; 563 | 564 | # Check the file 565 | my $file = shift or $self->_error( 'You did not specify a file name' ); 566 | 567 | my $fh; 568 | # flock if available (or warn if not possible for OS-specific reasons) 569 | if ( _can_flock() ) { 570 | # Open without truncation (truncate comes after lock) 571 | my $flags = Fcntl::O_WRONLY()|Fcntl::O_CREAT(); 572 | sysopen( $fh, $file, $flags ) 573 | or $self->_error("Failed to open file '$file' for writing: $!"); 574 | 575 | # Use no translation and strict UTF-8 576 | binmode( $fh, ":raw:encoding(UTF-8)"); 577 | 578 | flock( $fh, Fcntl::LOCK_EX() ) 579 | or warn "Couldn't lock '$file' for reading: $!"; 580 | 581 | # truncate and spew contents 582 | truncate $fh, 0; 583 | seek $fh, 0, 0; 584 | } 585 | else { 586 | open $fh, ">:unix:encoding(UTF-8)", $file; 587 | } 588 | 589 | # serialize and spew to the handle 590 | print {$fh} $self->_dump_string; 591 | 592 | # close the file (release the lock) 593 | unless ( close $fh ) { 594 | $self->_error("Failed to close file '$file': $!"); 595 | } 596 | 597 | return 1; 598 | } 599 | 600 | # Save an object to a string 601 | sub _dump_string { 602 | my $self = shift; 603 | return '' unless ref $self && @$self; 604 | 605 | # Iterate over the documents 606 | my $indent = 0; 607 | my @lines = (); 608 | 609 | eval { 610 | foreach my $cursor ( @$self ) { 611 | push @lines, '---'; 612 | 613 | # An empty document 614 | if ( ! defined $cursor ) { 615 | # Do nothing 616 | 617 | # A scalar document 618 | } elsif ( ! ref $cursor ) { 619 | $lines[-1] .= ' ' . $self->_dump_scalar( $cursor ); 620 | 621 | # A list at the root 622 | } elsif ( ref $cursor eq 'ARRAY' ) { 623 | unless ( @$cursor ) { 624 | $lines[-1] .= ' []'; 625 | next; 626 | } 627 | push @lines, $self->_dump_array( $cursor, $indent, {} ); 628 | 629 | # A hash at the root 630 | } elsif ( ref $cursor eq 'HASH' ) { 631 | unless ( %$cursor ) { 632 | $lines[-1] .= ' {}'; 633 | next; 634 | } 635 | push @lines, $self->_dump_hash( $cursor, $indent, {} ); 636 | 637 | } else { 638 | die \("Cannot serialize " . ref($cursor)); 639 | } 640 | } 641 | }; 642 | if ( ref $@ eq 'SCALAR' ) { 643 | $self->_error(${$@}); 644 | } elsif ( $@ ) { 645 | $self->_error($@); 646 | } 647 | 648 | join '', map { "$_\n" } @lines; 649 | } 650 | 651 | sub _has_internal_string_value { 652 | my $value = shift; 653 | my $b_obj = B::svref_2object(\$value); # for round trip problem 654 | return $b_obj->FLAGS & B::SVf_POK(); 655 | } 656 | 657 | sub _dump_scalar { 658 | my $string = $_[1]; 659 | my $is_key = $_[2]; 660 | # Check this before checking length or it winds up looking like a string! 661 | my $has_string_flag = _has_internal_string_value($string); 662 | return '~' unless defined $string; 663 | return "''" unless length $string; 664 | if (Scalar::Util::looks_like_number($string)) { 665 | # keys and values that have been used as strings get quoted 666 | if ( $is_key || $has_string_flag ) { 667 | return qq['$string']; 668 | } 669 | else { 670 | return $string; 671 | } 672 | } 673 | if ( $string =~ /[\x00-\x09\x0b-\x0d\x0e-\x1f\x7f-\x9f\'\n]/ ) { 674 | $string =~ s/\\/\\\\/g; 675 | $string =~ s/"/\\"/g; 676 | $string =~ s/\n/\\n/g; 677 | $string =~ s/[\x85]/\\N/g; 678 | $string =~ s/([\x00-\x1f])/\\$UNPRINTABLE[ord($1)]/g; 679 | $string =~ s/([\x7f-\x9f])/'\x' . sprintf("%X",ord($1))/ge; 680 | return qq|"$string"|; 681 | } 682 | if ( $string =~ /(?:^[~!@#%&*|>?:,'"`{}\[\]]|^-+$|\s|:\z)/ or 683 | $QUOTE{$string} 684 | ) { 685 | return "'$string'"; 686 | } 687 | return $string; 688 | } 689 | 690 | sub _dump_array { 691 | my ($self, $array, $indent, $seen) = @_; 692 | if ( $seen->{refaddr($array)}++ ) { 693 | die \"YAML::Tiny does not support circular references"; 694 | } 695 | my @lines = (); 696 | foreach my $el ( @$array ) { 697 | my $line = (' ' x $indent) . '-'; 698 | my $type = ref $el; 699 | if ( ! $type ) { 700 | $line .= ' ' . $self->_dump_scalar( $el ); 701 | push @lines, $line; 702 | 703 | } elsif ( $type eq 'ARRAY' ) { 704 | if ( @$el ) { 705 | push @lines, $line; 706 | push @lines, $self->_dump_array( $el, $indent + 1, $seen ); 707 | } else { 708 | $line .= ' []'; 709 | push @lines, $line; 710 | } 711 | 712 | } elsif ( $type eq 'HASH' ) { 713 | if ( keys %$el ) { 714 | push @lines, $line; 715 | push @lines, $self->_dump_hash( $el, $indent + 1, $seen ); 716 | } else { 717 | $line .= ' {}'; 718 | push @lines, $line; 719 | } 720 | 721 | } else { 722 | die \"YAML::Tiny does not support $type references"; 723 | } 724 | } 725 | 726 | @lines; 727 | } 728 | 729 | sub _dump_hash { 730 | my ($self, $hash, $indent, $seen) = @_; 731 | if ( $seen->{refaddr($hash)}++ ) { 732 | die \"YAML::Tiny does not support circular references"; 733 | } 734 | my @lines = (); 735 | foreach my $name ( sort keys %$hash ) { 736 | my $el = $hash->{$name}; 737 | my $line = (' ' x $indent) . $self->_dump_scalar($name, 1) . ":"; 738 | my $type = ref $el; 739 | if ( ! $type ) { 740 | $line .= ' ' . $self->_dump_scalar( $el ); 741 | push @lines, $line; 742 | 743 | } elsif ( $type eq 'ARRAY' ) { 744 | if ( @$el ) { 745 | push @lines, $line; 746 | push @lines, $self->_dump_array( $el, $indent + 1, $seen ); 747 | } else { 748 | $line .= ' []'; 749 | push @lines, $line; 750 | } 751 | 752 | } elsif ( $type eq 'HASH' ) { 753 | if ( keys %$el ) { 754 | push @lines, $line; 755 | push @lines, $self->_dump_hash( $el, $indent + 1, $seen ); 756 | } else { 757 | $line .= ' {}'; 758 | push @lines, $line; 759 | } 760 | 761 | } else { 762 | die \"YAML::Tiny does not support $type references"; 763 | } 764 | } 765 | 766 | @lines; 767 | } 768 | 769 | 770 | 771 | ##################################################################### 772 | # DEPRECATED API methods: 773 | 774 | # Error storage (DEPRECATED as of 1.57) 775 | our $errstr = ''; 776 | 777 | # Set error 778 | sub _error { 779 | require Carp; 780 | $errstr = $_[1]; 781 | $errstr =~ s/ at \S+ line \d+.*//; 782 | Carp::croak( $errstr ); 783 | } 784 | 785 | # Retrieve error 786 | my $errstr_warned; 787 | sub errstr { 788 | require Carp; 789 | Carp::carp( "YAML::Tiny->errstr and \$YAML::Tiny::errstr is deprecated" ) 790 | unless $errstr_warned++; 791 | $errstr; 792 | } 793 | 794 | 795 | 796 | 797 | ##################################################################### 798 | # Helper functions. Possibly not needed. 799 | 800 | 801 | # Use to detect nv or iv 802 | use B; 803 | 804 | # XXX-INGY Is flock YAML::Tiny's responsibility? 805 | # Some platforms can't flock :-( 806 | # XXX-XDG I think it is. When reading and writing files, we ought 807 | # to be locking whenever possible. People (foolishly) use YAML 808 | # files for things like session storage, which has race issues. 809 | my $HAS_FLOCK; 810 | sub _can_flock { 811 | if ( defined $HAS_FLOCK ) { 812 | return $HAS_FLOCK; 813 | } 814 | else { 815 | require Config; 816 | my $c = \%Config::Config; 817 | $HAS_FLOCK = grep { $c->{$_} } qw/d_flock d_fcntl_can_lock d_lockf/; 818 | require Fcntl if $HAS_FLOCK; 819 | return $HAS_FLOCK; 820 | } 821 | } 822 | 823 | 824 | # XXX-INGY Is this core in 5.8.1? Can we remove this? 825 | # XXX-XDG Scalar::Util 1.18 didn't land until 5.8.8, so we need this 826 | ##################################################################### 827 | # Use Scalar::Util if possible, otherwise emulate it 828 | 829 | use Scalar::Util (); 830 | BEGIN { 831 | local $@; 832 | if ( eval { Scalar::Util->VERSION(1.18); } ) { 833 | *refaddr = *Scalar::Util::refaddr; 834 | } 835 | else { 836 | eval <<'END_PERL'; 837 | # Scalar::Util failed to load or too old 838 | sub refaddr { 839 | my $pkg = ref($_[0]) or return undef; 840 | if ( !! UNIVERSAL::can($_[0], 'can') ) { 841 | bless $_[0], 'Scalar::Util::Fake'; 842 | } else { 843 | $pkg = undef; 844 | } 845 | "$_[0]" =~ /0x(\w+)/; 846 | my $i = do { no warnings 'portable'; hex $1 }; 847 | bless $_[0], $pkg if defined $pkg; 848 | $i; 849 | } 850 | END_PERL 851 | } 852 | } 853 | 854 | delete $YAML::Tiny::{refaddr}; 855 | 856 | 1; 857 | 858 | # XXX-INGY Doc notes I'm putting up here. Changing the doc when it's wrong 859 | # but leaving grey area stuff up here. 860 | # 861 | # I would like to change Read/Write to Load/Dump below without 862 | # changing the actual API names. 863 | # 864 | # It might be better to put Load/Dump API in the SYNOPSIS instead of the 865 | # dubious OO API. 866 | # 867 | # null and bool explanations may be outdated. 868 | 869 | __END__ 870 | 871 | =pod 872 | 873 | =head1 NAME 874 | 875 | YAML::Tiny - Read/Write YAML files with as little code as possible 876 | 877 | =head1 VERSION 878 | 879 | version 1.73 880 | 881 | =head1 PREAMBLE 882 | 883 | The YAML specification is huge. Really, B huge. It contains all the 884 | functionality of XML, except with flexibility and choice, which makes it 885 | easier to read, but with a formal specification that is more complex than 886 | XML. 887 | 888 | The original pure-Perl implementation L costs just over 4 megabytes 889 | of memory to load. Just like with Windows F<.ini> files (3 meg to load) and 890 | CSS (3.5 meg to load) the situation is just asking for a B 891 | module, an incomplete but correct and usable subset of the functionality, 892 | in as little code as possible. 893 | 894 | Like the other C<::Tiny> modules, YAML::Tiny has no non-core dependencies, 895 | does not require a compiler to install, is back-compatible to Perl v5.8.1, 896 | and can be inlined into other modules if needed. 897 | 898 | In exchange for this adding this extreme flexibility, it provides support 899 | for only a limited subset of YAML. But the subset supported contains most 900 | of the features for the more common uses of YAML. 901 | 902 | =head1 SYNOPSIS 903 | 904 | Assuming F like this: 905 | 906 | --- 907 | rootproperty: blah 908 | section: 909 | one: two 910 | three: four 911 | Foo: Bar 912 | empty: ~ 913 | 914 | 915 | Read and write F like this: 916 | 917 | use YAML::Tiny; 918 | 919 | # Open the config 920 | my $yaml = YAML::Tiny->read( 'file.yml' ); 921 | 922 | # Get a reference to the first document 923 | my $config = $yaml->[0]; 924 | 925 | # Or read properties directly 926 | my $root = $yaml->[0]->{rootproperty}; 927 | my $one = $yaml->[0]->{section}->{one}; 928 | my $Foo = $yaml->[0]->{section}->{Foo}; 929 | 930 | # Change data directly 931 | $yaml->[0]->{newsection} = { this => 'that' }; # Add a section 932 | $yaml->[0]->{section}->{Foo} = 'Not Bar!'; # Change a value 933 | delete $yaml->[0]->{section}; # Delete a value 934 | 935 | # Save the document back to the file 936 | $yaml->write( 'file.yml' ); 937 | 938 | To create a new YAML file from scratch: 939 | 940 | # Create a new object with a single hashref document 941 | my $yaml = YAML::Tiny->new( { wibble => "wobble" } ); 942 | 943 | # Add an arrayref document 944 | push @$yaml, [ 'foo', 'bar', 'baz' ]; 945 | 946 | # Save both documents to a file 947 | $yaml->write( 'data.yml' ); 948 | 949 | Then F will contain: 950 | 951 | --- 952 | wibble: wobble 953 | --- 954 | - foo 955 | - bar 956 | - baz 957 | 958 | =head1 DESCRIPTION 959 | 960 | B is a perl class for reading and writing YAML-style files, 961 | written with as little code as possible, reducing load time and memory 962 | overhead. 963 | 964 | Most of the time it is accepted that Perl applications use a lot 965 | of memory and modules. The B<::Tiny> family of modules is specifically 966 | intended to provide an ultralight and zero-dependency alternative to 967 | many more-thorough standard modules. 968 | 969 | This module is primarily for reading human-written files (like simple 970 | config files) and generating very simple human-readable files. Note that 971 | I said B and not B. The sort of files that 972 | your average manager or secretary should be able to look at and make 973 | sense of. 974 | 975 | =for stopwords normalise 976 | 977 | L does not generate comments, it won't necessarily preserve the 978 | order of your hashes, and it will normalise if reading in and writing out 979 | again. 980 | 981 | It only supports a very basic subset of the full YAML specification. 982 | 983 | =for stopwords embeddable 984 | 985 | Usage is targeted at files like Perl's META.yml, for which a small and 986 | easily-embeddable module is extremely attractive. 987 | 988 | Features will only be added if they are human readable, and can be written 989 | in a few lines of code. Please don't be offended if your request is 990 | refused. Someone has to draw the line, and for YAML::Tiny that someone 991 | is me. 992 | 993 | If you need something with more power move up to L (7 megabytes of 994 | memory overhead) or L (6 megabytes memory overhead and requires 995 | a C compiler). 996 | 997 | To restate, L does B preserve your comments, whitespace, 998 | or the order of your YAML data. But it should round-trip from Perl 999 | structure to file and back again just fine. 1000 | 1001 | =head1 METHODS 1002 | 1003 | =for Pod::Coverage HAVE_UTF8 refaddr 1004 | 1005 | =head2 new 1006 | 1007 | The constructor C creates a C object as a blessed array 1008 | reference. Any arguments provided are taken as separate documents 1009 | to be serialized. 1010 | 1011 | =head2 read $filename 1012 | 1013 | The C constructor reads a YAML file from a file name, 1014 | and returns a new C object containing the parsed content. 1015 | 1016 | Returns the object on success or throws an error on failure. 1017 | 1018 | =head2 read_string $string; 1019 | 1020 | The C constructor reads YAML data from a character string, and 1021 | returns a new C object containing the parsed content. If you have 1022 | read the string from a file yourself, be sure that you have correctly decoded 1023 | it into characters first. 1024 | 1025 | Returns the object on success or throws an error on failure. 1026 | 1027 | =head2 write $filename 1028 | 1029 | The C method generates the file content for the properties, and 1030 | writes it to disk using UTF-8 encoding to the filename specified. 1031 | 1032 | Returns true on success or throws an error on failure. 1033 | 1034 | =head2 write_string 1035 | 1036 | Generates the file content for the object and returns it as a character 1037 | string. This may contain non-ASCII characters and should be encoded 1038 | before writing it to a file. 1039 | 1040 | Returns true on success or throws an error on failure. 1041 | 1042 | =for stopwords errstr 1043 | 1044 | =head2 errstr (DEPRECATED) 1045 | 1046 | Prior to version 1.57, some errors were fatal and others were available only 1047 | via the C<$YAML::Tiny::errstr> variable, which could be accessed via the 1048 | C method. 1049 | 1050 | Starting with version 1.57, all errors are fatal and throw exceptions. 1051 | 1052 | The C<$errstr> variable is still set when exceptions are thrown, but 1053 | C<$errstr> and the C method are deprecated and may be removed in a 1054 | future release. The first use of C will issue a deprecation 1055 | warning. 1056 | 1057 | =head1 FUNCTIONS 1058 | 1059 | YAML::Tiny implements a number of functions to add compatibility with 1060 | the L API. These should be a drop-in replacement. 1061 | 1062 | =head2 Dump 1063 | 1064 | my $string = Dump(list-of-Perl-data-structures); 1065 | 1066 | Turn Perl data into YAML. This function works very much like 1067 | Data::Dumper::Dumper(). 1068 | 1069 | It takes a list of Perl data structures and dumps them into a serialized 1070 | form. 1071 | 1072 | It returns a character string containing the YAML stream. Be sure to encode 1073 | it as UTF-8 before serializing to a file or socket. 1074 | 1075 | The structures can be references or plain scalars. 1076 | 1077 | Dies on any error. 1078 | 1079 | =head2 Load 1080 | 1081 | my @data_structures = Load(string-containing-a-YAML-stream); 1082 | 1083 | Turn YAML into Perl data. This is the opposite of Dump. 1084 | 1085 | Just like L's thaw() function or the eval() function in relation 1086 | to L. 1087 | 1088 | It parses a character string containing a valid YAML stream into a list of 1089 | Perl data structures representing the individual YAML documents. Be sure to 1090 | decode the character string correctly if the string came from a file or 1091 | socket. 1092 | 1093 | my $last_data_structure = Load(string-containing-a-YAML-stream); 1094 | 1095 | For consistency with YAML.pm, when Load is called in scalar context, it 1096 | returns the data structure corresponding to the last of the YAML documents 1097 | found in the input stream. 1098 | 1099 | Dies on any error. 1100 | 1101 | =head2 freeze() and thaw() 1102 | 1103 | Aliases to Dump() and Load() for L fans. This will also allow 1104 | YAML::Tiny to be plugged directly into modules like POE.pm, that use the 1105 | freeze/thaw API for internal serialization. 1106 | 1107 | =head2 DumpFile(filepath, list) 1108 | 1109 | Writes the YAML stream to a file with UTF-8 encoding instead of just 1110 | returning a string. 1111 | 1112 | Dies on any error. 1113 | 1114 | =head2 LoadFile(filepath) 1115 | 1116 | Reads the YAML stream from a UTF-8 encoded file instead of a string. 1117 | 1118 | Dies on any error. 1119 | 1120 | =head1 YAML TINY SPECIFICATION 1121 | 1122 | This section of the documentation provides a specification for "YAML Tiny", 1123 | a subset of the YAML specification. 1124 | 1125 | It is based on and described comparatively to the YAML 1.1 Working Draft 1126 | 2004-12-28 specification, located at L. 1127 | 1128 | Terminology and chapter numbers are based on that specification. 1129 | 1130 | =head2 1. Introduction and Goals 1131 | 1132 | The purpose of the YAML Tiny specification is to describe a useful subset 1133 | of the YAML specification that can be used for typical document-oriented 1134 | use cases such as configuration files and simple data structure dumps. 1135 | 1136 | =for stopwords extensibility 1137 | 1138 | Many specification elements that add flexibility or extensibility are 1139 | intentionally removed, as is support for complex data structures, class 1140 | and object-orientation. 1141 | 1142 | In general, the YAML Tiny language targets only those data structures 1143 | available in JSON, with the additional limitation that only simple keys 1144 | are supported. 1145 | 1146 | As a result, all possible YAML Tiny documents should be able to be 1147 | transformed into an equivalent JSON document, although the reverse is 1148 | not necessarily true (but will be true in simple cases). 1149 | 1150 | =for stopwords PCRE 1151 | 1152 | As a result of these simplifications the YAML Tiny specification should 1153 | be implementable in a (relatively) small amount of code in any language 1154 | that supports Perl Compatible Regular Expressions (PCRE). 1155 | 1156 | =head2 2. Introduction 1157 | 1158 | YAML Tiny supports three data structures. These are scalars (in a variety 1159 | of forms), block-form sequences and block-form mappings. Flow-style 1160 | sequences and mappings are not supported, with some minor exceptions 1161 | detailed later. 1162 | 1163 | The use of three dashes "---" to indicate the start of a new document is 1164 | supported, and multiple documents per file/stream is allowed. 1165 | 1166 | Both line and inline comments are supported. 1167 | 1168 | Scalars are supported via the plain style, single quote and double quote, 1169 | as well as literal-style and folded-style multi-line scalars. 1170 | 1171 | The use of explicit tags is not supported. 1172 | 1173 | The use of "null" type scalars is supported via the ~ character. 1174 | 1175 | The use of "bool" type scalars is not supported. 1176 | 1177 | =for stopwords serializer 1178 | 1179 | However, serializer implementations should take care to explicitly escape 1180 | strings that match a "bool" keyword in the following set to prevent other 1181 | implementations that do support "bool" accidentally reading a string as a 1182 | boolean 1183 | 1184 | y|Y|yes|Yes|YES|n|N|no|No|NO 1185 | |true|True|TRUE|false|False|FALSE 1186 | |on|On|ON|off|Off|OFF 1187 | 1188 | The use of anchors and aliases is not supported. 1189 | 1190 | The use of directives is supported only for the %YAML directive. 1191 | 1192 | =head2 3. Processing YAML Tiny Information 1193 | 1194 | B 1195 | 1196 | =for stopwords deserialization 1197 | 1198 | The YAML specification dictates three-phase serialization and three-phase 1199 | deserialization. 1200 | 1201 | The YAML Tiny specification does not mandate any particular methodology 1202 | or mechanism for parsing. 1203 | 1204 | Any compliant parser is only required to parse a single document at a 1205 | time. The ability to support streaming documents is optional and most 1206 | likely non-typical. 1207 | 1208 | =for stopwords acyclic 1209 | 1210 | Because anchors and aliases are not supported, the resulting representation 1211 | graph is thus directed but (unlike the main YAML specification) B. 1212 | 1213 | Circular references/pointers are not possible, and any YAML Tiny serializer 1214 | detecting a circular reference should error with an appropriate message. 1215 | 1216 | B 1217 | 1218 | =for stopwords unicode 1219 | 1220 | YAML Tiny reads and write UTF-8 encoded files. Operations on strings expect 1221 | or produce Unicode characters not UTF-8 encoded bytes. 1222 | 1223 | B 1224 | 1225 | =for stopwords modality 1226 | 1227 | =for stopwords parsers 1228 | 1229 | YAML Tiny parsers and emitters are not expected to recover from, or 1230 | adapt to, errors. The specific error modality of any implementation is 1231 | not dictated (return codes, exceptions, etc.) but is expected to be 1232 | consistent. 1233 | 1234 | =head2 4. Syntax 1235 | 1236 | B 1237 | 1238 | YAML Tiny streams are processed in memory as Unicode characters and 1239 | read/written with UTF-8 encoding. 1240 | 1241 | The escaping and unescaping of the 8-bit YAML escapes is required. 1242 | 1243 | The escaping and unescaping of 16-bit and 32-bit YAML escapes is not 1244 | required. 1245 | 1246 | B 1247 | 1248 | Support for the "~" null/undefined indicator is required. 1249 | 1250 | Implementations may represent this as appropriate for the underlying 1251 | language. 1252 | 1253 | Support for the "-" block sequence indicator is required. 1254 | 1255 | Support for the "?" mapping key indicator is B required. 1256 | 1257 | Support for the ":" mapping value indicator is required. 1258 | 1259 | Support for the "," flow collection indicator is B required. 1260 | 1261 | Support for the "[" flow sequence indicator is B required, with 1262 | one exception (detailed below). 1263 | 1264 | Support for the "]" flow sequence indicator is B required, with 1265 | one exception (detailed below). 1266 | 1267 | Support for the "{" flow mapping indicator is B required, with 1268 | one exception (detailed below). 1269 | 1270 | Support for the "}" flow mapping indicator is B required, with 1271 | one exception (detailed below). 1272 | 1273 | Support for the "#" comment indicator is required. 1274 | 1275 | Support for the "&" anchor indicator is B required. 1276 | 1277 | Support for the "*" alias indicator is B required. 1278 | 1279 | Support for the "!" tag indicator is B required. 1280 | 1281 | Support for the "|" literal block indicator is required. 1282 | 1283 | Support for the ">" folded block indicator is required. 1284 | 1285 | Support for the "'" single quote indicator is required. 1286 | 1287 | Support for the """ double quote indicator is required. 1288 | 1289 | Support for the "%" directive indicator is required, but only 1290 | for the special case of a %YAML version directive before the 1291 | "---" document header, or on the same line as the document header. 1292 | 1293 | For example: 1294 | 1295 | %YAML 1.1 1296 | --- 1297 | - A sequence with a single element 1298 | 1299 | Special Exception: 1300 | 1301 | To provide the ability to support empty sequences 1302 | and mappings, support for the constructs [] (empty sequence) and {} 1303 | (empty mapping) are required. 1304 | 1305 | For example, 1306 | 1307 | %YAML 1.1 1308 | # A document consisting of only an empty mapping 1309 | --- {} 1310 | # A document consisting of only an empty sequence 1311 | --- [] 1312 | # A document consisting of an empty mapping within a sequence 1313 | - foo 1314 | - {} 1315 | - bar 1316 | 1317 | B 1318 | 1319 | Other than the empty sequence and mapping cases described above, YAML Tiny 1320 | supports only the indentation-based block-style group of contexts. 1321 | 1322 | All five scalar contexts are supported. 1323 | 1324 | Indentation spaces work as per the YAML specification in all cases. 1325 | 1326 | Comments work as per the YAML specification in all simple cases. 1327 | Support for indented multi-line comments is B required. 1328 | 1329 | Separation spaces work as per the YAML specification in all cases. 1330 | 1331 | B 1332 | 1333 | The only directive supported by the YAML Tiny specification is the 1334 | %YAML language/version identifier. Although detected, this directive 1335 | will have no control over the parsing itself. 1336 | 1337 | =for stopwords recognise 1338 | 1339 | The parser must recognise both the YAML 1.0 and YAML 1.1+ formatting 1340 | of this directive (as well as the commented form, although no explicit 1341 | code should be needed to deal with this case, being a comment anyway) 1342 | 1343 | That is, all of the following should be supported. 1344 | 1345 | --- #YAML:1.0 1346 | - foo 1347 | 1348 | %YAML:1.0 1349 | --- 1350 | - foo 1351 | 1352 | % YAML 1.1 1353 | --- 1354 | - foo 1355 | 1356 | Support for the %TAG directive is B required. 1357 | 1358 | Support for additional directives is B required. 1359 | 1360 | Support for the document boundary marker "---" is required. 1361 | 1362 | Support for the document boundary market "..." is B required. 1363 | 1364 | If necessary, a document boundary should simply by indicated with a 1365 | "---" marker, with not preceding "..." marker. 1366 | 1367 | Support for empty streams (containing no documents) is required. 1368 | 1369 | Support for implicit document starts is required. 1370 | 1371 | That is, the following must be equivalent. 1372 | 1373 | # Full form 1374 | %YAML 1.1 1375 | --- 1376 | foo: bar 1377 | 1378 | # Implicit form 1379 | foo: bar 1380 | 1381 | B 1382 | 1383 | Support for nodes optional anchor and tag properties is B required. 1384 | 1385 | Support for node anchors is B required. 1386 | 1387 | Support for node tags is B required. 1388 | 1389 | Support for alias nodes is B required. 1390 | 1391 | Support for flow nodes is B required. 1392 | 1393 | Support for block nodes is required. 1394 | 1395 | B 1396 | 1397 | Support for all five scalar styles is required as per the YAML 1398 | specification, although support for quoted scalars spanning more 1399 | than one line is B required. 1400 | 1401 | Support for multi-line scalar documents starting on the header 1402 | is not required. 1403 | 1404 | Support for the chomping indicators on multi-line scalar styles 1405 | is required. 1406 | 1407 | B 1408 | 1409 | Support for block-style sequences is required. 1410 | 1411 | Support for flow-style sequences is B required. 1412 | 1413 | Support for block-style mappings is required. 1414 | 1415 | Support for flow-style mappings is B required. 1416 | 1417 | Both sequences and mappings should be able to be arbitrarily 1418 | nested. 1419 | 1420 | Support for plain-style mapping keys is required. 1421 | 1422 | Support for quoted keys in mappings is B required. 1423 | 1424 | Support for "?"-indicated explicit keys is B required. 1425 | 1426 | =for stopwords endeth 1427 | 1428 | Here endeth the specification. 1429 | 1430 | =head2 Additional Perl-Specific Notes 1431 | 1432 | For some Perl applications, it's important to know if you really have a 1433 | number and not a string. 1434 | 1435 | That is, in some contexts is important that 3 the number is distinctive 1436 | from "3" the string. 1437 | 1438 | Because even Perl itself is not trivially able to understand the difference 1439 | (certainly without XS-based modules) Perl implementations of the YAML Tiny 1440 | specification are not required to retain the distinctiveness of 3 vs "3". 1441 | 1442 | =head1 SUPPORT 1443 | 1444 | Bugs should be reported via the CPAN bug tracker at 1445 | 1446 | L 1447 | 1448 | =begin html 1449 | 1450 | For other issues, or commercial enhancement or support, please contact 1451 | Adam Kennedy directly. 1452 | 1453 | =end html 1454 | 1455 | =head1 AUTHOR 1456 | 1457 | Adam Kennedy Eadamk@cpan.orgE 1458 | 1459 | =head1 SEE ALSO 1460 | 1461 | =over 4 1462 | 1463 | =item * L 1464 | 1465 | =item * L 1466 | 1467 | =item * L 1468 | 1469 | =item * L 1470 | 1471 | =item * L 1472 | 1473 | =item * L 1474 | 1475 | =back 1476 | 1477 | =head1 COPYRIGHT 1478 | 1479 | Copyright 2006 - 2013 Adam Kennedy. 1480 | 1481 | This program is free software; you can redistribute 1482 | it and/or modify it under the same terms as Perl itself. 1483 | 1484 | The full text of the license can be found in the 1485 | LICENSE file included with this module. 1486 | 1487 | =cut 1488 | --------------------------------------------------------------------------------