├── .gitignore ├── src ├── opam │ ├── install-opam-packages │ └── install-ocaml ├── os │ ├── alpine │ │ ├── create-user │ │ └── setup │ └── ubuntu │ │ ├── create-user │ │ └── setup └── create-dockerfile ├── common-config.sh ├── docker-push ├── .circleci ├── docker-login └── config.yml ├── Makefile ├── configs ├── ubuntu.sh └── alpine.sh ├── LICENSE ├── docker-build └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | tmp 2 | /pushme 3 | -------------------------------------------------------------------------------- /src/opam/install-opam-packages: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env bash 2 | # 3 | # Install opam packages specified on the command line. 4 | # From https://github.com/mjambon/setup-ocaml 5 | # 6 | set -eu 7 | 8 | packages=$(echo $* | sed -e 's/,/ /g') 9 | opam install -y $packages 10 | opam clean -y 11 | -------------------------------------------------------------------------------- /common-config.sh: -------------------------------------------------------------------------------- 1 | # To be included in configs/*.sh 2 | 3 | # Extra packages to be installed by the native package manager. 4 | 5 | # Alpine 6 | extra_apk_packages=" 7 | " 8 | 9 | # Ubuntu 10 | extra_deb_packages=" 11 | " 12 | 13 | # The collection of opam packages we want to install. Go wild. 14 | opam_packages=" 15 | dune 16 | utop 17 | " 18 | -------------------------------------------------------------------------------- /docker-push: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env bash 2 | # 3 | # Push Docker images to Docker Hub. 4 | # This pushes the images created with './docker-build' and stored to a 5 | # temporary file. 6 | # 7 | # From https://github.com/mjambon/setup-ocaml 8 | # 9 | # Usage: ./docker-push 10 | # 11 | # 12 | set -eu 13 | 14 | for docker_url in $(cat pushme); do 15 | echo "Pushing '$docker_url'." 16 | docker push "$docker_url" 17 | done 18 | -------------------------------------------------------------------------------- /.circleci/docker-login: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env bash 2 | # 3 | # Read docker registry credentials from environment variables, hoping 4 | # they don't end up in logs or bash history. 5 | # 6 | set -eu 7 | 8 | error() { 9 | echo "Error: $*" >&2 10 | exit 1 11 | } 12 | 13 | [[ -v DOCKERHUB_USER ]] || error "Cannot log in: DOCKERHUB_USER is unset." 14 | [[ -v DOCKERHUB_PASS ]] || error "Cannot log in: DOCKERHUB_PASS is unset." 15 | 16 | docker login -u "$DOCKERHUB_USER" -p "$DOCKERHUB_PASS" 17 | -------------------------------------------------------------------------------- /src/os/alpine/create-user: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env bash 2 | # 3 | # Create a non-privileged user on Alpine. 4 | # From https://github.com/mjambon/setup-ocaml 5 | # 6 | set -eu 7 | 8 | error() { 9 | echo "Error: $*" >&2 10 | exit 1 11 | } 12 | 13 | [[ $# = 1 ]] || error "Exactly one argument is expected, a user name." 14 | 15 | user="$1" 16 | 17 | if id -u "$user" > /dev/null 2>&1 ]]; then 18 | echo "Not creating user '$user' since it already exists." 19 | else 20 | echo "Creating user '$user'." 21 | addgroup "$user" 22 | adduser -S -u 1000 -g 1000 "$user" --shell /bin/bash 23 | echo "$user ALL=(ALL:ALL) NOPASSWD:ALL" > /etc/sudoers.d/"$user" 24 | chmod 440 /etc/sudoers.d/"$user" 25 | chown root:root /etc/sudoers.d/"$user" 26 | sed -i.bak 's/^Defaults.*requiretty/# \1/g' /etc/sudoers 27 | fi 28 | -------------------------------------------------------------------------------- /src/os/ubuntu/create-user: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env bash 2 | # 3 | # Create a non-privileged user on a Debian-based OS. 4 | # From https://github.com/mjambon/setup-ocaml 5 | # 6 | set -eu 7 | 8 | error() { 9 | echo "Error: $*" >&2 10 | exit 1 11 | } 12 | 13 | [[ $# = 1 ]] || error "Exactly one argument is expected, a user name." 14 | 15 | user="$1" 16 | 17 | if id -u "$user" > /dev/null 2>&1 ]]; then 18 | echo "Not creating user '$user' since it already exists." 19 | else 20 | echo "Creating user '$user'." 21 | addgroup "$user" 22 | adduser --ingroup "$user" --disabled-password "$user" --gecos '' 23 | echo "$user ALL=(ALL:ALL) NOPASSWD:ALL" > /etc/sudoers.d/"$user" 24 | chmod 440 /etc/sudoers.d/"$user" 25 | chown root:root /etc/sudoers.d/"$user" 26 | sed -i.bak 's/^Defaults.*requiretty//g' /etc/sudoers 27 | fi 28 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # 2 | # Usage: 3 | # - 'make' for building the Docker images specified by SELECTED_CONFIGS. 4 | # - 'make push' for pushing these images to your registry e.g. Docker Hub. 5 | # 6 | 7 | ifndef SELECTED_CONFIGS 8 | # The list of configuration files, one per image that you want to build 9 | # when running 'make' and 'make push'. 10 | SELECTED_CONFIGS = configs/alpine.sh 11 | endif 12 | export SELECTED_CONFIGS 13 | 14 | # Generate a Dockerfile for each config file and build a docker image. 15 | .PHONY: build 16 | build: 17 | ./docker-build $(SELECTED_CONFIGS) 18 | 19 | # Push the docker images to Docker Hub or some other registry. 20 | .PHONY: push 21 | push: 22 | ./docker-push 23 | 24 | # Build and push. 25 | .PHONY: all 26 | all: 27 | $(MAKE) build 28 | $(MAKE) push 29 | 30 | .PHONY: clean 31 | clean: 32 | git clean -dfX 33 | -------------------------------------------------------------------------------- /src/os/ubuntu/setup: -------------------------------------------------------------------------------- 1 | #! /bin/sh 2 | # 3 | # General system setup for Ubuntu prior to installing Opam. 4 | # From https://github.com/mjambon/setup-ocaml 5 | # 6 | set -eu 7 | 8 | # The command line arguments are a list of extra packages to install. 9 | extra_packages=$* 10 | 11 | apt-get update 12 | 13 | packages=" 14 | adduser 15 | bubblewrap 16 | build-essential 17 | curl 18 | git 19 | libcap-dev 20 | libx11-dev 21 | nano 22 | opam 23 | pre-commit 24 | rsync 25 | ssh 26 | sudo 27 | unzip 28 | $extra_packages 29 | " 30 | 31 | DEBIAN_FRONTEND=noninteractive apt-get install -y $packages --no-install-recommends 32 | DEBIAN_FRONTEND=noninteractive apt-get clean -y --no-install-recommends 33 | 34 | if [ -z "$(git config user.email)" ]; then 35 | git config --global user.email "docker@example.com" 36 | git config --global user.name "Docker" 37 | fi 38 | -------------------------------------------------------------------------------- /src/opam/install-ocaml: -------------------------------------------------------------------------------- 1 | #! /bin/sh 2 | # 3 | # Install opam, ocaml compilers and standard library. 4 | # From https://github.com/mjambon/setup-ocaml 5 | # 6 | set -eu 7 | 8 | mkdir -p .ssh 9 | chmod 700 .ssh 10 | 11 | if [ -z "$(git config user.email)" ]; then 12 | git config --global user.email "docker@example.com" 13 | git config --global user.name "Docker" 14 | fi 15 | 16 | echo -n "opam version: " 17 | opam --version 18 | 19 | echo "Initializing opam" 20 | opam init --disable-sandboxing --bare 21 | 22 | echo "Building OCaml compilers" 23 | opam switch create "$@" 24 | 25 | 26 | cat >> ~/.bashrc <<"EOF" 27 | # From https://github.com/mjambon/ocaml-layer 28 | echo 'Running "eval $(opam env)", which initializes PATH and other variables.' 29 | echo 30 | echo "In scripts and dockerfiles, don't forget to run your commands as" 31 | echo ' opam exec -- COMMAND' 32 | echo 'or equivalently as' 33 | echo ' eval $(opam env) && COMMAND' 34 | echo 35 | eval $(opam env) 36 | EOF 37 | -------------------------------------------------------------------------------- /src/os/alpine/setup: -------------------------------------------------------------------------------- 1 | #! /bin/sh 2 | # 3 | # General system setup for Alpine Linux prior to installing Opam. 4 | # From https://github.com/mjambon/setup-ocaml 5 | # 6 | set -eu 7 | 8 | # The command line arguments are a list of extra packages to install. 9 | extra_packages=$* 10 | 11 | echo '@testing https://dl-cdn.alpinelinux.org/alpine/edge/testing' \ 12 | >> /etc/apk/repositories 13 | 14 | packages=" 15 | bash 16 | bubblewrap 17 | build-base 18 | bzip2 19 | ca-certificates 20 | coreutils 21 | curl 22 | git 23 | libx11-dev 24 | m4 25 | nano 26 | ncurses-dev 27 | opam 28 | openssh 29 | openssl 30 | patch 31 | pre-commit 32 | py-pip 33 | rsync 34 | sudo 35 | tar 36 | xz 37 | $extra_packages 38 | " 39 | 40 | # can't have --no-cache here because https://github.com/ocaml/opam/issues/5186 41 | apk add $packages 42 | 43 | if [ -z "$(git config user.email)" ]; then 44 | git config --global user.email "docker@example.com" 45 | git config --global user.name "Docker" 46 | fi 47 | -------------------------------------------------------------------------------- /configs/ubuntu.sh: -------------------------------------------------------------------------------- 1 | # 2 | # Configuration for creating a dockerfile, building the docker image, and 3 | # for pushing it to a docker registry. 4 | # 5 | 6 | # Inherit common settings. 7 | . ./common-config.sh 8 | 9 | # The OS family. Determines which collection of install scripts to use. 10 | # Currently, choices are 'alpine' or 'ubuntu'. 11 | os="ubuntu" 12 | 13 | # The argument of the FROM line in the dockerfile. This is the docker 14 | # URL of the base image, optionally followed by more things. 15 | # 16 | from="ubuntu:24.04" 17 | 18 | # This is the argument of 'docker pull', 'docker push', etc. for the image 19 | # we are building. 20 | docker_url="mjambon/ocaml:ubuntu" 21 | 22 | # User to create and use. If it already exists, we'll try to use it. 23 | user="user" 24 | 25 | # Extra packages to be installed by the native package manager. 26 | extra_packages="$extra_deb_packages" 27 | 28 | # Opam switch to use. This determines the OCaml version and a set of 29 | # configuration options. 30 | opam_switch="5.2.1" 31 | 32 | # The collection of opam packages we want to install. Go wild. 33 | opam_packages="$opam_packages" 34 | -------------------------------------------------------------------------------- /configs/alpine.sh: -------------------------------------------------------------------------------- 1 | # 2 | # Configuration for creating a dockerfile, building the docker image, and 3 | # for pushing it to a docker registry. 4 | # 5 | 6 | # Inherit common settings. 7 | . ./common-config.sh 8 | 9 | # The OS family. Determines which collection of install scripts to use. 10 | # Currently, choices are 'alpine' or 'ubuntu'. 11 | os="alpine" 12 | 13 | # The argument of the FROM line in the dockerfile. This is the docker 14 | # URL of the base image, optionally followed by more things. 15 | # 16 | # Only pin to minor version, not patch to ensure rebuilds pull in security 17 | # fixes. 18 | from="alpine:3.21" 19 | 20 | # This is the argument of 'docker pull', 'docker push', etc. for the image 21 | # we are building. 22 | docker_url="mjambon/ocaml:alpine" 23 | 24 | # User to create and use. If it already exists, we'll try to use it. 25 | user="user" 26 | 27 | # Extra packages to be installed by the native package manager. 28 | extra_packages="$extra_apk_packages" 29 | 30 | # Opam switch to use. This determines the OCaml version and a set of 31 | # configuration options. 32 | opam_switch="5.2.1" 33 | 34 | # The collection of opam packages we want to install. Go wild. 35 | opam_packages="$opam_packages" 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | # 2 | # Rebuild and publish our base Docker images for OCaml once a week. 3 | # 4 | 5 | version: 2.1 6 | 7 | jobs: 8 | build-and-push: 9 | docker: 10 | - image: circleci/buildpack-deps:stretch 11 | steps: 12 | - checkout 13 | 14 | # Work around "no docker within no docker". 15 | - setup_remote_docker 16 | 17 | - run: 18 | name: Build 19 | command: make build 20 | - run: 21 | name: Login 22 | command: ./.circleci/docker-login 23 | - run: 24 | name: Push 25 | command: make push 26 | 27 | build-only: 28 | docker: 29 | - image: circleci/buildpack-deps:stretch 30 | steps: 31 | - checkout 32 | 33 | # Work around "no docker within no docker". 34 | - setup_remote_docker 35 | 36 | - run: 37 | name: Build 38 | command: make build 39 | 40 | workflows: 41 | version: 2 42 | 43 | build-on-push: 44 | jobs: 45 | - build-only 46 | 47 | build-and-push-weekly: 48 | # Rebuild periodically rather than based on git changes. 49 | triggers: 50 | - schedule: 51 | # Run at 10:00 every Wednesday, UTC. 52 | cron: "0 10 * * 3" 53 | filters: 54 | branches: 55 | only: 56 | - master 57 | - mjambon 58 | 59 | jobs: 60 | - build-and-push: 61 | # Use the CircleCI context that holds our DockerHub login 62 | # credentials and exposes them as environment variables 63 | # DOCKERHUB_USER and DOCKERHUB_PASS. 64 | context: dockerhub 65 | 66 | # Run only on these branches (each pushing different images) 67 | filters: 68 | branches: 69 | only: 70 | - master 71 | - mjambon 72 | -------------------------------------------------------------------------------- /docker-build: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env bash 2 | # 3 | # Build or update Docker images locally. 4 | # From https://github.com/mjambon/setup-ocaml 5 | # 6 | # Usage: ./docker-build [CONFIG_FILE1 CONFIG_FILE2 ...] 7 | # 8 | # Each config file is used separately to build a Docker image. 9 | # If no argument is given, a single config file 'config.sh' is assumed. 10 | # 11 | # The list of destination docker URLs (tags) is saved to a temporary file 12 | # which will be consulted our 'docker-push' script. 13 | # 14 | set -eu 15 | 16 | # YYYY-MM-DD date, made available to all the config files, which can 17 | # use it for tagging the docker images. 18 | # 19 | date=$(date +%F) 20 | 21 | # Defaults 22 | os="alpine" 23 | force_tag="$date" 24 | from="alpine:3.12.0" 25 | user="user" 26 | extra_packages="" 27 | opam_switch="4.14.0" 28 | opam_switch_options="" 29 | opam_packages="dune utop" 30 | 31 | docker_url="mjambon/ocaml:alpine" 32 | 33 | # This is a bash array. Requires bash >= 4.4. 34 | # Example: 35 | # 36 | # extra_docker_urls=("$docker_url-$date") 37 | # 38 | extra_docker_urls=() 39 | 40 | # Each command line argument is a path to a config file. 41 | configs="" 42 | if [[ $# -gt 0 ]]; then 43 | configs=$* 44 | fi 45 | 46 | build() { 47 | ( 48 | if [[ -f "$config" ]]; then 49 | echo "Build from config '$config'." 50 | . ./"$config" 51 | else 52 | echo "Build with default settings." 53 | fi 54 | 55 | rm -rf tmp 56 | mkdir -p tmp 57 | 58 | mkdir -p tmp/os 59 | cp -a src/os/"$os" tmp/os 60 | cp -a src/opam tmp 61 | cp -a src/create-dockerfile tmp 62 | 63 | extra_build_options=() 64 | for url in "${extra_docker_urls[@]}"; do 65 | extra_build_options+=( -t "$url" ) 66 | done 67 | 68 | ( 69 | cd tmp 70 | ./create-dockerfile -o Dockerfile \ 71 | --os "$os" \ 72 | --from "$from" \ 73 | --force-tag "$force_tag" \ 74 | --create-user "$user" \ 75 | --extra-packages "$extra_packages" \ 76 | --opam-packages "$opam_packages" \ 77 | --opam-switch "$opam_switch" \ 78 | --opam-switch-options "$opam_switch_options" 79 | docker build -t "$docker_url" "${extra_build_options[@]}" . 80 | 81 | # Sanity check. 82 | docker run -t "$docker_url" opam exec -- ocamlc -v 83 | ) 84 | 85 | echo "$docker_url" >> "$pushme" 86 | for url in "${extra_docker_urls[@]}"; do 87 | echo "$url" >> "$pushme" 88 | done 89 | ) 90 | } 91 | 92 | pushme=pushme 93 | rm -f "$pushme" 94 | for config in $configs; do 95 | build 96 | done 97 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ocaml-layer 2 | == 3 | [![CircleCI badge](https://circleci.com/gh/mjambon/ocaml-layer.svg?style=svg)](https://app.circleci.com/pipelines/github/mjambon/ocaml-layer) 4 | 5 | Bring your team's OCaml CI jobs down to 1 min. 6 | 7 | Motivation 8 | -- 9 | 10 | The goal is to set up a cached build environment on which your team can 11 | compile and test their own OCaml software, quickly every time. 12 | 13 | * Pre-install all the external dependencies - opam packages and more. 14 | * Start from the base Docker image of your choice. 15 | * It's admin-friendly. Maintenance requires no OCaml knowledge. 16 | * Customize by forking and editing this git repo. 17 | * It's Docker. Reproducible locally and not tied to a CI vendor's caching. 18 | 19 | Usage 20 | -- 21 | 22 | For evaluation purposes, you can simply run `make` and watch 23 | an image being built with some default settings. 24 | 25 | For actual use, follow these steps: 26 | 27 | 1. Create your own Docker repository on 28 | [Docker Hub](https://hub.docker.com/) or some other registry. 29 | 2. Fork this git repository. 30 | 3. Add packages to the lists in `common-config.sh`. 31 | 4. Create or edit one configuration file per container, in `configs/`. 32 | 5. Adjust the `SELECTED_CONFIGS` variable in the `Makefile`. 33 | 6. Run `make` to build the Docker images. 34 | 7. Run `make push` to upload the images. 35 | 8. Use these images as base images in your CI jobs. 36 | 37 | You can reuse and adapt the [CircleCI config](.circleci/config.yml) of 38 | this repo to rebuild your images on a weekly basis or so. 39 | 40 | Example 41 | -- 42 | 43 | The config I use for my own needs is 44 | [common-config.sh](https://github.com/mjambon/ocaml-layer/blob/mjambon/common-config.sh) 45 | and [configs](https://github.com/mjambon/ocaml-layer/tree/mjambon/configs). 46 | The Docker images end up on Docker Hub: 47 | [minimum OCaml version](https://hub.docker.com/r/mjambon/mj-ocaml-4.02/tags) 48 | and [latest OCaml](https://hub.docker.com/r/mjambon/mj-ocaml/tags). 49 | They are updated weekly using CircleCI 50 | ([config](https://github.com/mjambon/ocaml-layer/blob/mjambon/.circleci/config.yml)). 51 | 52 | Suggestions 53 | -- 54 | 55 | * If you're an individual open-source developer, you may want to 56 | target two versions of OCaml: the minimum version that you're 57 | willing to support and the latest version. 58 | * The [ocaml/opam2 images](https://hub.docker.com/r/ocaml/opam2/) 59 | already support many flavors of operating systems and are 60 | graciously maintained for you. You probably should use those if the 61 | speed of routine CI builds isn't a priority for you. 62 | -------------------------------------------------------------------------------- /src/create-dockerfile: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env bash 2 | # 3 | # Put together a Dockerfile from options specified on the command line. 4 | # From https://github.com/mjambon/setup-ocaml 5 | # 6 | set -eu -o pipefail 7 | 8 | default_cmd='bash' 9 | default_os=alpine 10 | default_extra_packages='' 11 | default_opam_packages='dune' 12 | default_opam_switch='5.2.1' 13 | default_opam_switch_options='' 14 | 15 | usage() { 16 | cat <&2 78 | exit 1 79 | } 80 | 81 | os="$default_os" 82 | cmd="$default_cmd" 83 | no_cmd=false 84 | from='' 85 | no_from=false 86 | create_user='' 87 | dockerfile='' 88 | extra_packages="$default_extra_packages" 89 | force_tag='' 90 | opam_packages="$default_opam_packages" 91 | opam_switch="$default_opam_switch" 92 | opam_switch_options="$default_opam_switch_options" 93 | 94 | # The commands like '$(echo $...)' are for squashing multiline input into 95 | # a single line, for compatibility with dockerfile syntax. At the time 96 | # of writing the dockerfiles, certains things like lists of packages will 97 | # be formatted as multiple lines regardless. 98 | # 99 | while [[ $# -gt 0 ]]; do 100 | case "$1" in 101 | --cmd) 102 | cmd=$(echo $2) 103 | shift 104 | ;; 105 | --from) 106 | from=$(echo $2) 107 | shift 108 | ;; 109 | --create-user) 110 | create_user=$(echo $2) 111 | shift 112 | ;; 113 | --extra-packages) 114 | extra_packages=$(echo $2) 115 | shift 116 | ;; 117 | --force-tag) 118 | force_tag=$(echo $2) 119 | shift 120 | ;; 121 | --help) 122 | usage 123 | exit 0 124 | ;; 125 | --no-cmd) 126 | no_cmd=true 127 | ;; 128 | --no-from) 129 | no_from=true 130 | ;; 131 | -o) 132 | dockerfile=$2 133 | shift 134 | ;; 135 | --opam-packages) 136 | opam_packages=$(echo $2) 137 | shift 138 | ;; 139 | --opam-switch) 140 | opam_switch=$(echo $2) 141 | shift 142 | ;; 143 | --opam-switch-options) 144 | opam_switch_options=$(echo $2) 145 | shift 146 | ;; 147 | --os) 148 | os=$2 149 | case "$os" in 150 | alpine|ubuntu) 151 | ;; 152 | *) 153 | error "Unsupported value for --os: '$os'" 154 | esac 155 | shift 156 | ;; 157 | *) 158 | error "Unrecognized command line argument: '$1'" 159 | esac 160 | shift 161 | done 162 | 163 | if [[ -z "$from" ]]; then 164 | case "$os" in 165 | alpine) 166 | from="alpine" 167 | ;; 168 | ubuntu) 169 | from="ubuntu" 170 | ;; 171 | *) 172 | error "Cannot derive 'FROM' field from OS value '$os'. Try '--from'." 173 | esac 174 | fi 175 | 176 | # Print one argument per line, ending each line except the last by a backslash. 177 | # 178 | multiline() { 179 | while [[ $# -gt 1 ]]; do 180 | echo "$1"' \' 181 | echo -n " " 182 | shift 183 | done 184 | if [[ $# = 1 ]]; then 185 | echo "$1" 186 | fi 187 | } 188 | 189 | print_create_user() { 190 | case "$create_user" in 191 | '') 192 | echo 193 | ;; 194 | root) 195 | cat < "$dockerfile" 212 | fi 213 | 214 | cat <