diff --git a/README.md b/README.md index f4f5f7148..b159f4110 100644 --- a/README.md +++ b/README.md @@ -42,27 +42,61 @@ ## Quickstart (CLI) +To install: + +```sh +# With --dryrun the install script will print the commands it will use +# to install without actually installing so you know what it's going to do. +curl -fsSL https://d2lang.com/install.sh | sh -s -- --dryrun +# If things look good, install for real. +curl -fsSL https://d2lang.com/install.sh | sh -s -- +``` + The most convenient way to use D2 is to just run it as a CLI executable to produce SVGs from `.d2` files. ```sh -go install oss.terrastruct.com/d2 - echo 'x -> y -> z' > in.d2 d2 --watch in.d2 out.svg ``` A browser window will open with `out.svg` and live-reload on changes to `in.d2`. -### MacOS +### Installing from source -Homebrew package coming soon. +```sh +go install oss.terrastruct.com/d2 +``` -### Linux/Windows +### Install We have precompiled binaries on the [releases](https://github.com/terrastruct/d2/releases) -page. D2 will be added to OS-respective package managers soon. +page for macOS and Linux. For both amd64 and arm64. We will release package manager +distributions like .rpm, .deb soon. We also want to get D2 on Homebrew for macOS +and release a docker image +For now, if you don't want to install from source, just use our install script: +Pass `--tala` if you want to install our improved but closed source layout engine +tala. See the docs on [layout engine](#layout-engine) below. + +```sh +# With --dryrun the install script will print the commands it will use +# to install without actually installing so you know what it's going to do. +curl -fsSL https://d2lang.com/install.sh | sh -s -- --dryrun +# If things look good, install for real. +curl -fsSL https://d2lang.com/install.sh | sh -s -- +``` + +To uninstall: + +```sh +curl -fsSL https://d2lang.com/install.sh | sh -s -- --uninstall --dryrun +# If things look good, install for real. +curl -fsSL https://d2lang.com/install.sh | sh -s -- --uninstall +``` + +> warn: Our binary releases aren't fully portable like normal Go binaries due to the C +> dependency on v8go for executing dagre. ## Quickstart (library) @@ -155,6 +189,14 @@ any single type like "radial" or "tree" (as almost all layout engines are). For information and to download & try TALA, see [https://github.com/terrastruct/TALA](https://github.com/terrastruct/TALA). +You can just pass `--tala` to the install script to install tala as well: + +``` +curl -fsSL https://d2lang.com/install.sh | sh -s -- --tala --dryrun +# If things look good, install for real. +curl -fsSL https://d2lang.com/install.sh | sh -s -- --tala +``` + ## Comparison For a comparison against other popular text-to-diagram tools, see diff --git a/ci/release/README.md b/ci/release/README.md new file mode 100644 index 000000000..4530983e4 --- /dev/null +++ b/ci/release/README.md @@ -0,0 +1,49 @@ +# release + +## _install.sh + +The template for the install script in the root of the repository. + +### gen_install.sh + +Generates the install.sh script in the root of the repository by prepending the libraries +it depends on from ../sub/lib. + +## release.sh + +- ./release.sh is the top level script to generate a new release. + Run with --help for usage. + +## build.sh + +- ./build.sh builds the release archives for each platform into ./build//*.tar.gz + Run with --help for usage. + +> note: Remember for production releases you need to set the $TSTRUCT_OS_ARCH_BUILDER +> variables as we must compile d2 directly on each release target to include dagre. +> See https://github.com/terrastruct/d2/issues/31 + +Use `--host-only` to build only the release for the host's OS-ARCH pair. + +### build_docker.sh + +Helper script called by build.sh to build D2 on each linux runner inside Docker. +The Dockerfile is in ./builders/Dockerfile + +### _build.sh + +Called by build.sh (with --local or macOS) or build_docker.sh (on linux) to create the +release archive. + +Do not invoke directly. If you want to produce a build for a single platform run build.sh +as so: + +```sh + # To only build the linux-amd64 release. +./build.sh --run=linux-amd64 +``` + +```sh + # To only build the linux-amd64 release locally. +./build.sh --local --run=linux-amd64 +``` diff --git a/ci/release/_build.sh b/ci/release/_build.sh new file mode 100755 index 000000000..c5170219a --- /dev/null +++ b/ci/release/_build.sh @@ -0,0 +1,22 @@ +#!/bin/sh +set -eu +cd -- "$(dirname "$0")/../.." +. ./ci/sub/lib.sh + +sh_c mkdir -p "$HW_BUILD_DIR" +sh_c rsync --recursive --perms --delete \ + --human-readable --copy-links ./ci/release/template/ "$HW_BUILD_DIR/" +VERSION=$VERSION sh_c eval "'$HW_BUILD_DIR/README.md.sh'" \> "'$HW_BUILD_DIR/README.md'" +sh_c rm -f "$HW_BUILD_DIR/README.md.sh" +sh_c find "$HW_BUILD_DIR" -exec touch {} \\\; + +export GOOS=$(goos "$OS") +export GOARCH="$ARCH" +sh_c mkdir -p "$HW_BUILD_DIR/bin" +sh_c go build -ldflags "'-X oss.terrastruct.com/d2/lib/version.Version=$VERSION'" \ + -o "$HW_BUILD_DIR/bin/d2" ./cmd/d2 + +ARCHIVE=$PWD/$ARCHIVE +cd "$(dirname "$HW_BUILD_DIR")" +sh_c tar -czf "$ARCHIVE" "$(basename "$HW_BUILD_DIR")" +cd ->/dev/null diff --git a/ci/release/_install.sh b/ci/release/_install.sh new file mode 100755 index 000000000..4637489f0 --- /dev/null +++ b/ci/release/_install.sh @@ -0,0 +1,377 @@ +#!/bin/sh +set -eu + +cd -- "$(dirname "$0")/../sub/lib" +. ./log.sh +. ./flag.sh +. ./release.sh +cd - >/dev/null + +help() { + arg0="$0" + if [ "$0" = sh ]; then + arg0="curl -fsSL https://d2lang.com/install.sh | sh -s --" + fi + + cat < but the release archive in + ~/.cache/d2/release will remain. + +--uninstall: + Uninstall the installed version of d2. The --method and --prefix flags must be the same + as for installation. i.e if you used --method standalone you must again use --method + standalone for uninstallation. With detect, the install script will try to use the OS + package manager to uninstall instead. + +All downloaded archives are cached into ~/.cache/d2/release. use \$XDG_CACHE_HOME to change +path of the cached assets. Release archives are unarchived into /usr/local/lib/d2/d2- + +note: Deleting the unarchived releases will cause --uninstall to stop working. + +You can rerun install.sh to update your version of D2. install.sh will avoid reinstalling +if the installed version is the latest unless --force is passed. +EOF +} + +main() { + if [ -n "${DEBUG-}" ]; then + set -x + fi + + METHOD=standalone + while :; do + flag_parse "$@" + case "$FLAG" in + h|help) + help + return 0 + ;; + dry-run) + flag_noarg && shift "$FLAGSHIFT" + DRY_RUN=1 + ;; + version) + flag_nonemptyarg && shift "$FLAGSHIFT" + VERSION=$FLAGARG + ;; + tala) + shift "$FLAGSHIFT" + TALA=${FLAGARG:-latest} + ;; + edge) + flag_noarg && shift "$FLAGSHIFT" + EDGE=1 + echoerr "$FLAGRAW is currently unimplemented" + return 1 + ;; + method) + flag_nonemptyarg && shift "$FLAGSHIFT" + METHOD=$FLAGARG + echoerr "$FLAGRAW is currently unimplemented" + return 1 + ;; + prefix) + flag_nonemptyarg && shift "$FLAGSHIFT" + export PREFIX=$FLAGARG + ;; + force) + flag_noarg && shift "$FLAGSHIFT" + FORCE=1 + ;; + uninstall) + flag_noarg && shift "$FLAGSHIFT" + UNINSTALL=1 + ;; + '') + shift "$FLAGSHIFT" + break + ;; + *) + flag_errusage "unrecognized flag $FLAGRAW" + ;; + esac + done + + if [ $# -gt 0 ]; then + flag_errusage "no arguments are accepted" + fi + + REPO=${REPO:-terrastruct/d2} + PREFIX=${PREFIX:-/usr/local} + OS=$(os) + ARCH=$(arch) + CACHE_DIR=$(cache_dir) + mkdir -p "$CACHE_DIR" + INSTALL_DIR=$PREFIX/lib/d2 + + if [ -n "${UNINSTALL-}" ]; then + uninstall + return 0 + fi + + VERSION=${VERSION:-latest} + if [ "$VERSION" = latest ]; then + header "fetching latest release info" + fetch_release_info + fi + + install +} + +install() { + install_d2 + if [ "${TALA-}" ]; then + # Run in subshell to avoid overwriting VERSION. + ( install_tala ) + fi + + COLOR=2 header success + log "d2-$VERSION-$OS-$ARCH has been successfully installed into $PREFIX" + if ! echo "$PATH" | grep -qF "$PREFIX/bin"; then + logcat >&2 </dev/null; then + INSTALLED_VERSION="$(d2 version)" + if [ ! "${FORCE-}" -a "$VERSION" = "$INSTALLED_VERSION" ]; then + log "skipping installation as version $VERSION is already installed." + return 0 + fi + log "uninstalling $INSTALLED_VERSION to install $VERSION" + if ! uninstall_d2; then + warn "failed to uninstall $INSTALLED_VERSION" + fi + fi + + header "installing d2-$VERSION" + install_standalone_d2 +} + +install_standalone_d2() { + ARCHIVE="d2-$VERSION-$OS-$ARCH.tar.gz" + log "installing standalone release $ARCHIVE from github" + + fetch_release_info + asset_line=$(cat "$RELEASE_INFO" | grep -n "$ARCHIVE" | cut -d: -f1 | head -n1) + asset_url=$(sed -n $((asset_line-3))p "$RELEASE_INFO" | sed 's/^.*: "\(.*\)",$/\1/g') + fetch_gh "$asset_url" "$CACHE_DIR/$ARCHIVE" 'application/octet-stream' + + sh_c="sh_c" + if ! is_prefix_writable; then + sh_c="sudo_sh_c" + fi + + "$sh_c" tar -C "$INSTALL_DIR" -xzf "$CACHE_DIR/$ARCHIVE" + "$sh_c" sh -c "'cd \"$INSTALL_DIR/d2-$VERSION\" && make install PREFIX=\"$PREFIX\"'" +} + +install_tala() { + install_standalone_tala +} + +install_standalone_tala() { + REPO="${REPO_TALA:-terrastruct/TALA}" + VERSION=$TALA + RELEASE_INFO= + fetch_release_info + + ARCHIVE="tala-$VERSION-$OS-$ARCH.tar.gz" + log "installing standalone release $ARCHIVE from github" + + asset_line=$(cat "$RELEASE_INFO" | grep -n "$ARCHIVE" | cut -d: -f1 | head -n1) + asset_url=$(sed -n $((asset_line-3))p "$RELEASE_INFO" | sed 's/^.*: "\(.*\)",$/\1/g') + + fetch_gh "$asset_url" "$CACHE_DIR/$ARCHIVE" 'application/octet-stream' + + sh_c="sh_c" + if ! is_prefix_writable; then + sh_c="sudo_sh_c" + fi + + "$sh_c" tar -C "$INSTALL_DIR" -xzf "$CACHE_DIR/$ARCHIVE" + "$sh_c" sh -c "'cd \"$INSTALL_DIR/tala-$VERSION\" && make install PREFIX=\"$PREFIX\"'" +} + +uninstall() { + if ! command -v d2 >/dev/null; then + echoerr "no version of d2 installed" + return 1 + fi + INSTALLED_VERSION="$(d2 --version)" + if ! uninstall_d2; then + echoerr "failed to uninstall $INSTALLED_VERSION" + return 1 + fi + if [ "${TALA-}" ]; then + if ! command -v d2plugin-tala >/dev/null; then + echoerr "no version of tala installed" + return 1 + fi + INSTALLED_VERSION="$(d2plugin-tala --version)" + if ! uninstall_tala; then + echoerr "failed to uninstall tala $INSTALLED_VERSION" + return 1 + fi + fi + return 0 +} + +uninstall_d2() { + header "uninstalling d2-$INSTALLED_VERSION" + uninstall_standalone_d2 +} + +uninstall_standalone_d2() { + log "uninstalling standalone release of d2-$INSTALLED_VERSION" + + if [ ! -e "$INSTALL_DIR/d2-$INSTALLED_VERSION" ]; then + echoerr "missing standalone install release directory $INSTALL_DIR/d2-$INSTALLED_VERSION" + return 1 + fi + + sh_c="sh_c" + if ! is_prefix_writable; then + sh_c="sudo_sh_c" + fi + + "$sh_c" sh -c "'cd \"$INSTALL_DIR/d2-$INSTALLED_VERSION\" && make uninstall PREFIX=\"$PREFIX\"'" + "$sh_c" rm -rf "$INSTALL_DIR/d2-$INSTALLED_VERSION" +} + +uninstall_tala() { + uninstall_standalone_tala +} + +uninstall_standalone_tala() { + log "uninstalling standalone release tala-$INSTALLED_VERSION" + + if [ ! -e "$INSTALL_DIR/tala-$INSTALLED_VERSION" ]; then + echoerr "missing standalone install release directory $INSTALL_DIR/tala-$INSTALLED_VERSION" + return 1 + fi + + sh_c="sh_c" + if ! is_prefix_writable; then + sh_c="sudo_sh_c" + fi + + "$sh_c" sh -c "'cd \"$INSTALL_DIR/tala-$INSTALLED_VERSION\" && make uninstall PREFIX=\"$PREFIX\"'" + "$sh_c" rm -rf "$INSTALL_DIR/tala-$INSTALLED_VERSION" +} + +is_prefix_writable() { + sh_c "mkdir -p '$INSTALL_DIR' 2>/dev/null" || true + # The reason for checking whether bin is writable specifically is that on macOS you have + # /usr/local owned by root but you don't need root to write to its subdirectories which + # is all we want to do. + if [ ! -w "$PREFIX/bin" ]; then + return 0 + fi +} + +cache_dir() { + if [ -n "${XDG_CACHE_HOME-}" ]; then + echo "$XDG_CACHE_HOME/d2/release" + elif [ -n "${HOME-}" ]; then + echo "$HOME/.cache/d2/release" + else + echo "/tmp/d2-cache/release" + fi +} + +fetch_release_info() { + if [ -n "${RELEASE_INFO-}" ]; then + return 0 + fi + + log "fetching info on $VERSION version of $REPO" + RELEASE_INFO=$(mktemp -d)/release-info.json + if [ "$VERSION" = latest ]; then + release_info_url="https://api.github.com/repos/$REPO/releases/$VERSION" + else + release_info_url="https://api.github.com/repos/$REPO/releases/tags/$VERSION" + fi + fetch_gh "$release_info_url" "$RELEASE_INFO" \ + 'application/json' + VERSION=$(cat "$RELEASE_INFO" | grep -m1 tag_name | sed 's/^.*: "\(.*\)",$/\1/g') +} + +curl_gh() { + sh_c curl -fL ${GITHUB_TOKEN+"-H \"Authorization: Bearer \$GITHUB_TOKEN\""} "$@" +} + +fetch_gh() { + url=$1 + file=$2 + accept=$3 + + if [ -e "$file" ]; then + log "reusing $file" + return + fi + + curl_gh -#o "$file.inprogress" -C- -H "'Accept: $accept'" "$url" + sh_c mv "$file.inprogress" "$file" +} + +main "$@" diff --git a/ci/release/build.sh b/ci/release/build.sh new file mode 100755 index 000000000..be40bd835 --- /dev/null +++ b/ci/release/build.sh @@ -0,0 +1,192 @@ +#!/bin/sh +set -eu +cd -- "$(dirname "$0")/../.." +. ./ci/sub/lib.sh + +help() { + cat </d2---.tar.gz + +The version is detected via git describe which will use the git tag for the current +commit if available. + +Flags: + +--rebuild + By default build.sh will avoid rebuilding finished assets if they already exist but if you + changed something and need to force rebuild, use this flag. + +--local + By default build.sh uses \$TSTRUCT_MACOS_AMD64_BUILDER, \$TSTRUCT_MACOS_ARM64_BUILDER, + \$TSTRUCT_LINUX_AMD64_BUILDER and \$TSTRUCT_LINUX_ARM64_BUILDER to build the release + archives. It's required for now due to the following issue: + https://github.com/terrastruct/d2/issues/31 With --local, build.sh will cross compile + locally. warning: This is only for testing purposes, do not use in production! + +--host-only + Use to build the release archive for the host OS-ARCH only. All logging is done to stderr + so in a script you can read from stdout to get the path to the release archive. + +--run=regex + Use to run only the OS-ARCH jobs that match the given regex. e.g. --run=linux only runs + the linux jobs. --run=linux-amd64 only runs the linux-amd64 job. + +--version vX.X.X + Use to overwrite the version detected from git. +EOF +} + +main() { + while :; do + flag_parse "$@" + case "$FLAG" in + h|help) + help + return 0 + ;; + rebuild) + flag_noarg && shift "$FLAGSHIFT" + REBUILD=1 + ;; + local) + flag_noarg && shift "$FLAGSHIFT" + LOCAL=1 + ;; + dry-run) + flag_noarg && shift "$FLAGSHIFT" + DRY_RUN=1 + ;; + run) + flag_reqarg && shift "$FLAGSHIFT" + JOBFILTER="$FLAGARG" + ;; + host-only) + flag_noarg && shift "$FLAGSHIFT" + HOST_ONLY=1 + LOCAL=1 + ;; + version) + flag_nonemptyarg && shift "$FLAGSHIFT" + VERSION=$FLAGARG + ;; + '') + shift "$FLAGSHIFT" + break + ;; + *) + flag_errusage "unrecognized flag $FLAGRAW" + ;; + esac + done + + if [ $# -gt 0 ]; then + flag_errusage "no arguments are accepted" + fi + + VERSION=${VERSION:-$(git_describe_ref)} + BUILD_DIR=ci/release/build/$VERSION + if [ -n "${HOST_ONLY-}" ]; then + runjob $(os)-$(arch) "OS=$(os) ARCH=$(arch) build" & + waitjobs + return 0 + fi + + runjob linux-amd64 'OS=linux ARCH=amd64 build' & + runjob linux-arm64 'OS=linux ARCH=arm64 build' & + runjob macos-amd64 'OS=macos ARCH=amd64 build' & + runjob macos-arm64 'OS=macos ARCH=arm64 build' & + waitjobs +} + +build() { + HW_BUILD_DIR="$BUILD_DIR/$OS-$ARCH/d2-$VERSION" + ARCHIVE="$BUILD_DIR/d2-$VERSION-$OS-$ARCH.tar.gz" + + if [ -e "$ARCHIVE" -a -z "${REBUILD-}" ]; then + log "skipping as already built at $ARCHIVE" + return 0 + fi + + if [ -n "${LOCAL-}" ]; then + build_local + return 0 + fi + + case $OS in + macos) + case $ARCH in + amd64) + RHOST=$TSTRUCT_MACOS_AMD64_BUILDER build_rhost_macos + ;; + arm64) + RHOST=$TSTRUCT_MACOS_ARM64_BUILDER build_rhost_macos + ;; + *) + warn "no builder for OS=$OS ARCH=$ARCH, building locally..." + build_local + ;; + esac + ;; + linux) + case $ARCH in + amd64) + RHOST=$TSTRUCT_LINUX_AMD64_BUILDER build_rhost_linux + ;; + arm64) + RHOST=$TSTRUCT_LINUX_ARM64_BUILDER build_rhost_linux + ;; + *) + warn "no builder for OS=$OS ARCH=$ARCH, building locally..." + build_local + ;; + esac + ;; + *) + warn "no builder for OS=$OS, building locally..." + build_local + ;; + esac +} + +build_local() { + export DRY_RUN \ + HW_BUILD_DIR \ + VERSION \ + OS \ + ARCH \ + ARCHIVE + sh_c ./ci/release/_build.sh +} + +build_rhost_macos() { + sh_c ssh "$RHOST" mkdir -p src + sh_c rsync --archive --human-readable --delete ./ "$RHOST:src/d2/" + sh_c ssh -tttt "$RHOST" "DRY_RUN=${DRY_RUN-} \ +HW_BUILD_DIR=$HW_BUILD_DIR \ +VERSION=$VERSION \ +OS=$OS \ +ARCH=$ARCH \ +ARCHIVE=$ARCHIVE \ +TERM=$TERM \ +PATH=\"/usr/local/bin:/usr/local/sbin\${PATH+:\$PATH}\" \ +./src/d2/ci/release/_build.sh" + sh_c rsync --archive --human-readable "$RHOST:src/d2/$ARCHIVE" "$ARCHIVE" +} + +build_rhost_linux() { + sh_c ssh "$RHOST" mkdir -p src + sh_c rsync --archive --human-readable --delete ./ "$RHOST:src/d2/" + sh_c ssh -tttt "$RHOST" "DRY_RUN=${DRY_RUN-} \ +HW_BUILD_DIR=$HW_BUILD_DIR \ +VERSION=$VERSION \ +OS=$OS \ +ARCH=$ARCH \ +ARCHIVE=$ARCHIVE \ +TERM=$TERM \ +./src/d2/ci/release/build_docker.sh" + sh_c rsync --archive --human-readable "$RHOST:src/d2/$ARCHIVE" "$ARCHIVE" +} + +main "$@" diff --git a/ci/release/build/.gitignore b/ci/release/build/.gitignore new file mode 100644 index 000000000..72e8ffc0d --- /dev/null +++ b/ci/release/build/.gitignore @@ -0,0 +1 @@ +* diff --git a/ci/release/build_docker.sh b/ci/release/build_docker.sh new file mode 100755 index 000000000..7c3751af2 --- /dev/null +++ b/ci/release/build_docker.sh @@ -0,0 +1,17 @@ +#!/bin/sh +set -eu +cd -- "$(dirname "$0")/../.." +. ./ci/sub/lib.sh + +tag="$(sh_c docker build \ + --build-arg GOVERSION="1.19.3.linux-$ARCH" \ + -qf ./ci/release/builders/Dockerfile ./ci/release/builders)" +docker_run \ + -e DRY_RUN \ + -e HW_BUILD_DIR \ + -e VERSION \ + -e OS \ + -e ARCH \ + -e ARCHIVE \ + -e TERM \ + "$tag" ./src/d2/ci/release/_build.sh diff --git a/ci/release/builders/Dockerfile b/ci/release/builders/Dockerfile new file mode 100644 index 000000000..5adc0bd25 --- /dev/null +++ b/ci/release/builders/Dockerfile @@ -0,0 +1,12 @@ +FROM debian:10 + +RUN apt-get update +RUN apt-get install -y curl + +ARG GOVERSION= +RUN curl -fsSL "https://go.dev/dl/go$GOVERSION.tar.gz" >/tmp/go.tar.gz +RUN tar -C /usr/local -xzf /tmp/go.tar.gz +ENV PATH="/usr/local/go/bin:$PATH" + +RUN apt-get install -y build-essential +RUN apt-get install -y rsync diff --git a/ci/release/builders/Dockerfile-centos b/ci/release/builders/Dockerfile-centos new file mode 100644 index 000000000..66aaf6f17 --- /dev/null +++ b/ci/release/builders/Dockerfile-centos @@ -0,0 +1,16 @@ +FROM centos:7 + +ARG GOVERSION= + +RUN curl -fsSL "https://go.dev/dl/go$GOVERSION.tar.gz" >/tmp/go.tar.gz +RUN tar -C /usr/local -xzf /tmp/go.tar.gz + +ENV PATH="/usr/local/go/bin:$PATH" + +RUN yum install -y rsync wget +RUN yum groupinstall -y 'Development Tools' + +RUN curl -fsSL https://ftp.gnu.org/gnu/gcc/gcc-5.2.0/gcc-5.2.0.tar.gz >/tmp/gcc.tar.gz +RUN tar -C /usr/local -xzf /tmp/gcc.tar.gz +RUN cd /usr/local/gcc-5.2.0 && ./contrib/download_prerequisites && mkdir -p build \ + && cd build && ../configure --disable-multilib && make && make install diff --git a/ci/release/builders/aws_ensure.sh b/ci/release/builders/aws_ensure.sh new file mode 100755 index 000000000..d1b1a531c --- /dev/null +++ b/ci/release/builders/aws_ensure.sh @@ -0,0 +1,236 @@ +#!/bin/sh +set -eu +cd -- "$(dirname "$0")/../../.." +. ./ci/sub/lib.sh + +help() { + cat </dev/null \ + | jq -r .SecurityGroups[0].GroupId) + if [ -z "$SG_ID" ]; then + SG_ID=$(sh_c aws ec2 create-security-group \ + --group-name ssh \ + --description ssh \ + --vpc-id "$VPC_ID" | jq -r .GroupId) + fi + + header security-group-ingress + SG_RULES_COUNT=$(aws ec2 describe-security-groups --group-names ssh \ + | jq -r '.SecurityGroups[0].IpPermissions | length') + if [ "$SG_RULES_COUNT" -eq 0 ]; then + sh_c aws ec2 authorize-security-group-ingress \ + --group-id "$SG_ID" \ + --protocol tcp \ + --port 22 \ + --cidr 0.0.0.0/0 >/dev/null + fi + + header linux-amd64 + state=$(aws ec2 describe-instances --filters \ + 'Name=instance-state-name,Values=pending,running,stopping,stopped' 'Name=tag:Name,Values=d2-builder-linux-amd64' \ + | jq -r '.Reservations[].Instances[].State.Name') + if [ -z "$state" ]; then + sh_c aws ec2 run-instances \ + --image-id=ami-0d593311db5abb72b \ + --count=1 \ + --instance-type=t2.small \ + --security-groups=ssh \ + "--key-name=$KEY_NAME" \ + --tag-specifications '"ResourceType=instance,Tags=[{Key=Name,Value=d2-builder-linux-amd64}]"' \ + '"ResourceType=volume,Tags=[{Key=Name,Value=d2-builder-linux-amd64}]"' >/dev/null + fi + while true; do + dnsname=$(sh_c aws ec2 describe-instances \ + --filters 'Name=instance-state-name,Values=pending,running,stopping,stopped' 'Name=tag:Name,Values=d2-builder-linux-amd64' \ + | jq -r '.Reservations[].Instances[].PublicDnsName') + if [ -n "$dnsname" ]; then + log "TSTRUCT_LINUX_AMD64_BUILDER=ec2-user@$dnsname" + export TSTRUCT_LINUX_AMD64_BUILDER=ec2-user@$dnsname + break + fi + sleep 5 + done + + header linux-arm64 + state=$(aws ec2 describe-instances --filters \ + 'Name=instance-state-name,Values=pending,running,stopping,stopped' 'Name=tag:Name,Values=d2-builder-linux-arm64' \ + | jq -r '.Reservations[].Instances[].State.Name') + if [ -z "$state" ]; then + sh_c aws ec2 run-instances \ + --image-id=ami-0efabcf945ffd8831 \ + --count=1 \ + --instance-type=t4g.small \ + --security-groups=ssh \ + "--key-name=$KEY_NAME" \ + --tag-specifications '"ResourceType=instance,Tags=[{Key=Name,Value=d2-builder-linux-arm64}]"' \ + '"ResourceType=volume,Tags=[{Key=Name,Value=d2-builder-linux-arm64}]"' >/dev/null + fi + while true; do + dnsname=$(sh_c aws ec2 describe-instances \ + --filters 'Name=instance-state-name,Values=pending,running,stopping,stopped' 'Name=tag:Name,Values=d2-builder-linux-arm64' \ + | jq -r '.Reservations[].Instances[].PublicDnsName') + if [ -n "$dnsname" ]; then + log "TSTRUCT_LINUX_ARM64_BUILDER=ec2-user@$dnsname" + export TSTRUCT_LINUX_ARM64_BUILDER=ec2-user@$dnsname + break + fi + sleep 5 + done + + header "macos-amd64-host" + MACOS_AMD64_HOST_ID=$(aws ec2 describe-hosts --filter 'Name=state,Values=pending,available' 'Name=tag:Name,Values=d2-builder-macos-amd64' | jq -r '.Hosts[].HostId') + if [ -z "$MACOS_AMD64_HOST_ID" ]; then + MACOS_AMD64_HOST_ID=$(sh_c aws ec2 allocate-hosts --instance-type mac1.metal --quantity 1 --availability-zone us-west-2a \ + --tag-specifications '"ResourceType=dedicated-host,Tags=[{Key=Name,Value=d2-builder-macos-amd64}]"' \ + | jq -r .HostIds[0]) + fi + + header "macos-arm64-host" + MACOS_ARM64_HOST_ID=$(aws ec2 describe-hosts --filter 'Name=state,Values=pending,available' 'Name=tag:Name,Values=d2-builder-macos-arm64' | jq -r '.Hosts[].HostId') + if [ -z "$MACOS_ARM64_HOST_ID" ]; then + MACOS_ARM64_HOST_ID=$(sh_c aws ec2 allocate-hosts --instance-type mac2.metal --quantity 1 --availability-zone us-west-2a \ + --tag-specifications '"ResourceType=dedicated-host,Tags=[{Key=Name,Value=d2-builder-macos-amd64}]"' \ + | jq -r .HostIds[0]) + fi + + header macos-amd64 + state=$(aws ec2 describe-instances --filters \ + 'Name=instance-state-name,Values=pending,running,stopping,stopped' 'Name=tag:Name,Values=d2-builder-macos-amd64' \ + | jq -r '.Reservations[].Instances[].State.Name') + if [ -z "$state" ]; then + sh_c aws ec2 run-instances \ + --image-id=ami-0dd2ded7568750663 \ + --count=1 \ + --instance-type=mac1.metal \ + --security-groups=ssh \ + "--key-name=$KEY_NAME" \ + --placement "Tenancy=host,HostId=$MACOS_AMD64_HOST_ID" \ + --tag-specifications '"ResourceType=instance,Tags=[{Key=Name,Value=d2-builder-macos-amd64}]"' \ + '"ResourceType=volume,Tags=[{Key=Name,Value=d2-builder-macos-amd64}]"' >/dev/null + fi + while true; do + dnsname=$(sh_c aws ec2 describe-instances \ + --filters 'Name=instance-state-name,Values=pending,running,stopping,stopped' 'Name=tag:Name,Values=d2-builder-macos-amd64' \ + | jq -r '.Reservations[].Instances[].PublicDnsName') + if [ -n "$dnsname" ]; then + log "TSTRUCT_MACOS_AMD64_BUILDER=ec2-user@$dnsname" + export TSTRUCT_MACOS_AMD64_BUILDER=ec2-user@$dnsname + break + fi + sleep 5 + done + + header macos-arm64 + state=$(aws ec2 describe-instances --filters \ + 'Name=instance-state-name,Values=pending,running,stopping,stopped' 'Name=tag:Name,Values=d2-builder-macos-arm64' \ + | jq -r '.Reservations[].Instances[].State.Name') + if [ -z "$state" ]; then + sh_c aws ec2 run-instances \ + --image-id=ami-0af0516ff2c43dbbe \ + --count=1 \ + --instance-type=mac2.metal \ + --security-groups=ssh \ + "--key-name=$KEY_NAME" \ + --placement "Tenancy=host,HostId=$MACOS_ARM64_HOST_ID" \ + --tag-specifications '"ResourceType=instance,Tags=[{Key=Name,Value=d2-builder-macos-arm64}]"' \ + '"ResourceType=volume,Tags=[{Key=Name,Value=d2-builder-macos-arm64}]"' >/dev/null + fi + while true; do + dnsname=$(sh_c aws ec2 describe-instances \ + --filters 'Name=instance-state-name,Values=pending,running,stopping,stopped' 'Name=tag:Name,Values=d2-builder-macos-arm64' \ + | jq -r '.Reservations[].Instances[].PublicDnsName') + if [ -n "$dnsname" ]; then + log "TSTRUCT_MACOS_ARM64_BUILDER=ec2-user@$dnsname" + export TSTRUCT_MACOS_ARM64_BUILDER=ec2-user@$dnsname + break + fi + sleep 5 + done +} + +init_rhosts() { + header linux-amd64 + RHOST=$TSTRUCT_LINUX_AMD64_BUILDER init_rhost_linux + header linux-arm64 + RHOST=$TSTRUCT_LINUX_ARM64_BUILDER init_rhost_linux + header macos-amd64 + RHOST=$TSTRUCT_MACOS_AMD64_BUILDER init_rhost_macos + header macos-arm64 + RHOST=$TSTRUCT_MACOS_ARM64_BUILDER init_rhost_macos + + COLOR=2 header summary + log "export TSTRUCT_LINUX_AMD64_BUILDER=$TSTRUCT_LINUX_AMD64_BUILDER" + log "export TSTRUCT_LINUX_ARM64_BUILDER=$TSTRUCT_LINUX_ARM64_BUILDER" + log "export TSTRUCT_MACOS_AMD64_BUILDER=$TSTRUCT_MACOS_AMD64_BUILDER" + log "export TSTRUCT_MACOS_ARM64_BUILDER=$TSTRUCT_MACOS_ARM64_BUILDER" +} + +init_rhost_linux() { + while true; do + if sh_c ssh "$RHOST" :; then + break + fi + sleep 5 + done + sh_c ssh "$RHOST" 'sudo yum upgrade -y' + sh_c ssh "$RHOST" 'sudo yum install -y docker' + sh_c ssh "$RHOST" 'sudo systemctl start docker' + sh_c ssh "$RHOST" 'sudo systemctl enable docker' + sh_c ssh "$RHOST" 'sudo usermod -a -G docker ec2-user' + sh_c ssh "$RHOST" 'sudo reboot' || true +} + +init_rhost_macos() { + while true; do + if sh_c ssh "$RHOST" :; then + break + fi + sleep 5 + done + sh_c ssh "$RHOST" '": | /bin/bash -c \"\$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)\""' + sh_c ssh "$RHOST" '/usr/local/bin/brew update' + sh_c ssh "$RHOST" '/usr/local/bin/brew upgrade' + sh_c ssh "$RHOST" '/usr/local/bin/brew install go' +} + +main "$@" diff --git a/ci/release/builders/aws_ssh_copy_id.sh b/ci/release/builders/aws_ssh_copy_id.sh new file mode 100755 index 000000000..d50a96d9d --- /dev/null +++ b/ci/release/builders/aws_ssh_copy_id.sh @@ -0,0 +1,43 @@ +#!/bin/sh +set -eu +cd -- "$(dirname "$0")/../../.." +. ./ci/sub/lib.sh + +help() { + cat <./install.sh <\> ./install.sh +sh_c cat ./ci/release/_install.sh \ + \| sed -n "'/cd -- \"\$(dirname/,/cd -/!p'" \>\> install.sh diff --git a/ci/release/release.sh b/ci/release/release.sh new file mode 100755 index 000000000..517ae778d --- /dev/null +++ b/ci/release/release.sh @@ -0,0 +1,5 @@ +#!/bin/sh +set -eu +cd -- "$(dirname "$0")/../.." + +./ci/sub/release/release.sh "$@" diff --git a/ci/release/template/LICENSE.txt b/ci/release/template/LICENSE.txt new file mode 120000 index 000000000..6932c6369 --- /dev/null +++ b/ci/release/template/LICENSE.txt @@ -0,0 +1 @@ +../../../LICENSE.txt \ No newline at end of file diff --git a/ci/release/template/Makefile b/ci/release/template/Makefile new file mode 100644 index 000000000..5b9ba1353 --- /dev/null +++ b/ci/release/template/Makefile @@ -0,0 +1,12 @@ +.POSIX: +.SILENT: + +PREFIX = $(DESTDIR)/usr/local + +.PHONY: install +install: + PREFIX='$(PREFIX)' ./scripts/install.sh + +.PHONY: uninstall +uninstall: + PREFIX='$(PREFIX)' ./scripts/uninstall.sh diff --git a/ci/release/template/README.md.sh b/ci/release/template/README.md.sh new file mode 100755 index 000000000..ec7cb8a11 --- /dev/null +++ b/ci/release/template/README.md.sh @@ -0,0 +1,28 @@ +#!/bin/sh +set -eu + +cat < "$seed_file" + shuf -i "$range" -n 1 --random-source="$seed_file" +} + +pick() { + seed="$1" + shift + i="$(rand "$seed" "1-$#")" + eval "_echo \"\$$i\"" +} + +tput() { + if [ -n "$TERM" ]; then + command tput "$@" + fi +} + +setaf() { + tput setaf "$1" + shift + printf '%s' "$*" + tput sgr0 +} + +_echo() { + printf '%s\n' "$*" +} + +get_rand_color() { + # 1-6 are regular and 9-14 are bright. + # 1,2 and 9,10 are red and green but we use those for success and failure. + pick "$*" 3 4 5 6 11 12 13 14 +} + +echop() { + prefix="$1" + shift + + if [ "$#" -gt 0 ]; then + printfp "$prefix" "%s\n" "$*" + else + printfp "$prefix" + printf '\n' + fi +} + +printfp() {( + prefix="$1" + shift + + if [ -z "${COLOR:-}" ]; then + COLOR="$(get_rand_color "$prefix")" + fi + printf '%s' "$(setaf "$COLOR" "$prefix")" + + if [ $# -gt 0 ]; then + printf ': ' + printf "$@" + fi +)} + +catp() { + prefix="$1" + shift + + printfp "$prefix" + printf ': ' + read -r line + _echo "$line" + + indent=$(repeat ' ' 2) + sed "s/^/$indent/" +} + +repeat() { + char="$1" + times="$2" + seq -s "$char" "$times" | tr -d '[:digit:]' +} + +strlen() { + printf %s "$1" | wc -c +} + +echoerr() { + COLOR=1 echop err "$*" >&2 +} + +caterr() { + COLOR=1 catp err "$@" >&2 +} + +printferr() { + COLOR=1 printfp err "$@" >&2 +} + +logp() { + echop "$@" >&2 +} + +logfp() { + printfp "$@" >&2 +} + +logpcat() { + catp "$@" >&2 +} + +log() { + COLOR=5 logp log "$@" +} + +logf() { + COLOR=5 logfp log "$@" +} + +logcat() { + COLOR=5 catp log "$@" >&2 +} + +sh_c() { + COLOR=3 logp exec "$*" + if [ -z "${DRY_RUN-}" ]; then + "$@" + fi +} + +header() { + logp "/* $1 */" +} diff --git a/ci/release/template/scripts/uninstall.sh b/ci/release/template/scripts/uninstall.sh new file mode 100755 index 000000000..a41b449c9 --- /dev/null +++ b/ci/release/template/scripts/uninstall.sh @@ -0,0 +1,16 @@ +#!/bin/sh +set -eu +cd -- "$(dirname "$0")/.." +. ./scripts/lib.sh + +main() { + if [ ! -e "${PREFIX-}" ]; then + echoerr "\$PREFIX must be set to a unix prefix directory from which to uninstall d2 like /usr/local" + return 1 + fi + + sh_c rm -f "$PREFIX/bin/d2" + sh_c rm -f "$PREFIX/share/man/man1" +} + +main "$@" diff --git a/ci/sub b/ci/sub index 7ff838089..be28ce747 160000 --- a/ci/sub +++ b/ci/sub @@ -1 +1 @@ -Subproject commit 7ff8380897435e73d53e329a7cd39dc38c7ad227 +Subproject commit be28ce747b280c59b8fe89e6b29bb0637984985d diff --git a/cmd/d2/help.go b/cmd/d2/help.go index affc2abf5..961c9c275 100644 --- a/cmd/d2/help.go +++ b/cmd/d2/help.go @@ -27,7 +27,7 @@ Subcommands: %[1]s layout - Lists available layout engine options with short help %[1]s layout [layout name] - Display long help for a particular layout engine -See more docs at https://oss.terrastruct.com/d2 +See more docs and the source code at https://oss.terrastruct.com/d2 `, ms.Name, ms.FlagHelp()) } diff --git a/go.mod b/go.mod index 0977ac236..f0c96a553 100644 --- a/go.mod +++ b/go.mod @@ -28,7 +28,7 @@ require ( oss.terrastruct.com/xjson v0.0.0-20221018000420-4986731c4c4a oss.terrastruct.com/xos v0.0.0-20221018030138-c96e7ae96e5d oss.terrastruct.com/xrand v0.0.0-20221020211818-4ac08e618333 - rogchap.com/v8go v0.7.0 + rogchap.com/v8go v0.7.1-0.20221102201510-1f00b5007d95 ) require ( diff --git a/go.sum b/go.sum index b27d23c8d..ac8c2599d 100644 --- a/go.sum +++ b/go.sum @@ -786,8 +786,8 @@ oss.terrastruct.com/xos v0.0.0-20221018030138-c96e7ae96e5d h1:rrPTkbAfsRTW1WLoTz oss.terrastruct.com/xos v0.0.0-20221018030138-c96e7ae96e5d/go.mod h1:uSONPDInIwglnC+0zYs8YOjiUD8ZUSnqDTTI82j7Oro= oss.terrastruct.com/xrand v0.0.0-20221020211818-4ac08e618333 h1:7EdxwXM75Id1VIN71QbE8bLzZRMs0qD7olnDw5gbI7w= oss.terrastruct.com/xrand v0.0.0-20221020211818-4ac08e618333/go.mod h1:O7TAoBmlQhoi46RdgVikDcoLRb/vLflhkXCAd+nO4SM= -rogchap.com/v8go v0.7.0 h1:kgjbiO4zE5itA962ze6Hqmbs4HgZbGzmueCXsZtremg= -rogchap.com/v8go v0.7.0/go.mod h1:MxgP3pL2MW4dpme/72QRs8sgNMmM0pRc8DPhcuLWPAs= +rogchap.com/v8go v0.7.1-0.20221102201510-1f00b5007d95 h1:r89YHVIWeQj/A3Nu6462eqARUECJlJkLRk36pfML1xA= +rogchap.com/v8go v0.7.1-0.20221102201510-1f00b5007d95/go.mod h1:MxgP3pL2MW4dpme/72QRs8sgNMmM0pRc8DPhcuLWPAs= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= rsc.io/pdf v0.1.1 h1:k1MczvYDUvJBe93bYd7wrZLLUEcLZAuF824/I4e5Xr4= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= diff --git a/install.sh b/install.sh new file mode 100755 index 000000000..f564dd04e --- /dev/null +++ b/install.sh @@ -0,0 +1,791 @@ +#!/bin/sh +set -eu + +# ************* +# DO NOT EDIT +# +# install.sh was bundled together from +# +# - ./ci/sub/lib/rand.sh +# - ./ci/sub/lib/log.sh +# - ./ci/sub/lib/flag.sh +# - ./ci/sub/lib/release.sh +# - ./ci/release/_install.sh +# +# The last of which implements the installation logic. +# +# Generated by ./ci/release/gen_install.sh. +# ************* + +#!/bin/sh +if [ "${LIB_RAND-}" ]; then + return 0 +fi +LIB_RAND=1 + +rand() { + seed="$1" + range="$2" + + seed_file="$(mktemp)" + _echo "$seed" | md5sum > "$seed_file" + shuf -i "$range" -n 1 --random-source="$seed_file" +} + +pick() { + seed="$1" + shift + i="$(rand "$seed" "1-$#")" + eval "_echo \"\$$i\"" +} +#!/bin/sh +if [ "${LIB_LOG-}" ]; then + return 0 +fi +LIB_LOG=1 + +tput() { + if [ -n "$TERM" ]; then + command tput "$@" + fi +} + +setaf() { + tput setaf "$1" + shift + printf '%s' "$*" + tput sgr0 +} + +_echo() { + printf '%s\n' "$*" +} + +get_rand_color() { + # 1-6 are regular and 9-14 are bright. + # 1,2 and 9,10 are red and green but we use those for success and failure. + pick "$*" 3 4 5 6 11 12 13 14 +} + +echop() { + prefix="$1" + shift + + if [ "$#" -gt 0 ]; then + printfp "$prefix" "%s\n" "$*" + else + printfp "$prefix" + printf '\n' + fi +} + +printfp() {( + prefix="$1" + shift + + if [ -z "${COLOR:-}" ]; then + COLOR="$(get_rand_color "$prefix")" + fi + printf '%s' "$(setaf "$COLOR" "$prefix")" + + if [ $# -gt 0 ]; then + printf ': ' + printf "$@" + fi +)} + +catp() { + prefix="$1" + shift + + sed "s/^/$(printfp "$prefix" '')/" +} + +repeat() { + char="$1" + times="$2" + seq -s "$char" "$times" | tr -d '[:digit:]' +} + +strlen() { + printf %s "$1" | wc -c +} + +echoerr() { + COLOR=1 echop err "$*" | humanpath>&2 +} + +caterr() { + COLOR=1 catp err "$@" | humanpath >&2 +} + +printferr() { + COLOR=1 printfp err "$@" | humanpath >&2 +} + +logp() { + echop "$@" | humanpath >&2 +} + +logfp() { + printfp "$@" | humanpath >&2 +} + +logpcat() { + catp "$@" | humanpath >&2 +} + +log() { + COLOR=5 logp log "$@" +} + +logf() { + COLOR=5 logfp log "$@" +} + +logcat() { + COLOR=5 catp log "$@" >&2 +} + +warn() { + COLOR=3 logp warn "$@" +} + +warnf() { + COLOR=3 logfp warn "$@" +} + +sh_c() { + COLOR=3 logp exec "$*" + if [ -z "${DRY_RUN-}" ]; then + eval "$@" + fi +} + +sudo_sh_c() { + if [ "$(id -u)" -eq 0 ]; then + sh_c "$@" + elif command -v doas >/dev/null; then + sh_c "doas $*" + elif command -v sudo >/dev/null; then + sh_c "sudo $*" + elif command -v su >/dev/null; then + sh_c "su root -c '$*'" + else + caterr <"$out" 2>&1 + code="$?" + set -e + if [ "$code" -eq 0 ]; then + return + fi + cat "$out" >&2 + exit "$code" +} + +echo_dur() { + local dur=$1 + local h=$((dur/60/60)) + local m=$((dur/60%60)) + local s=$((dur%60)) + printf '%dh%dm%ds' "$h" "$m" "$s" +} + +sponge() { + dst="$1" + tmp="$(mktemp)" + cat > "$tmp" + cat "$tmp" > "$dst" +} + +stripansi() { + # First regex gets rid of standard xterm escape sequences for controlling + # visual attributes. + # The second regex I'm not 100% sure, the reference says it selects the US + # encoding but I'm not sure why that's necessary or why it always occurs + # in tput sgr0 before the standard escape sequence. + # See tput sgr0 | xxd + sed -e $'s/\x1b\[[0-9;]*m//g' -e $'s/\x1b(.//g' +} + +runtty() { + case "$(uname)" in + Darwin) + script -q /dev/null "$@" + ;; + Linux) + script -eqc "$*" + ;; + *) + echoerr "runtty: unsupported OS $(uname)" + return 1 + esac +} +#!/bin/sh +if [ "${LIB_FLAG-}" ]; then + return 0 +fi +LIB_FLAG=1 + +# flag_parse implements a robust flag parser. +# +# For a full fledge example see ../examples/date.sh +# +# It differs from getopts(1) in that long form options are supported. Currently the only +# deficiency is that short combined options are not supported like -xyzq. That would be +# interpreted as a single -xyzq flag. The other deficiency is lack of support for short +# flag syntax like -carg where the arg is not separated from the flag. This one is +# unfixable I believe unfortunately but for combined short flags I have opened +# https://github.com/terrastruct/ci/issues/6 +# +# flag_parse stores state in $FLAG, $FLAGRAW, $FLAGARG and $FLAGSHIFT. +# FLAG contains the name of the flag without hyphens. +# FLAGRAW contains the name of the flag as passed in with hyphens. +# FLAGARG contains the argument for the flag if there was any. +# If there was none, it will not be set. +# FLAGSHIFT contains the number by which the arguments should be shifted to +# start at the next flag/argument +# +# After each call check $FLAG for the name of the parsed flag. +# If empty, then no more flags are left. +# Still, call shift "$FLAGSHIFT" in case there was a -- +# +# If the argument for the flag is optional, then use ${FLAGARG-} to access +# the argument if one was passed. Use ${FLAGARG+x} = x to check if it was set. +# You only need to explicitly check if the flag was set if you care whether the user +# explicitly passed the empty string as the argument. +# +# Otherwise, call one of the flag_*arg functions: +# +# If a flag requires an argument, call flag_reqarg +# - $FLAGARG is guaranteed to be set after. +# If a flag requires a non empty argument, call flag_nonemptyarg +# - $FLAGARG is guaranteed to be set to a non empty string after. +# If a flag should not be passed an argument, call flag_noarg +# - $FLAGARG is guaranteed to be unset after. +# +# And then shift "$FLAGSHIFT" +flag_parse() { + case "${1-}" in + -*=*) + # Remove everything after first equal sign. + FLAG="${1%%=*}" + # Remove leading hyphens. + FLAG="${FLAG#-}"; FLAG="${FLAG#-}" + FLAGRAW="$(flag_fmt)" + # Remove everything before first equal sign. + FLAGARG="${1#*=}" + FLAGSHIFT=1 + ;; + -) + FLAG= + FLAGRAW= + unset FLAGARG + FLAGSHIFT=0 + ;; + --) + FLAG= + FLAGRAW= + unset FLAGARG + FLAGSHIFT=1 + ;; + -*) + # Remove leading hyphens. + FLAG="${1#-}"; FLAG="${FLAG#-}" + FLAGRAW=$(flag_fmt) + unset FLAGARG + FLAGSHIFT=1 + if [ $# -gt 1 ]; then + case "$2" in + -) + FLAGARG="$2" + FLAGSHIFT=2 + ;; + -*) + ;; + *) + FLAGARG="$2" + FLAGSHIFT=2 + ;; + esac + fi + ;; + *) + FLAG= + FLAGRAW= + unset FLAGARG + FLAGSHIFT=0 + ;; + esac + return 0 +} + +flag_reqarg() { + if [ "${FLAGARG+x}" != x ]; then + flag_errusage "flag $FLAGRAW requires an argument" + fi +} + +flag_nonemptyarg() { + flag_reqarg + if [ -z "$FLAGARG" ]; then + flag_errusage "flag $FLAGRAW requires a non-empty argument" + fi +} + +flag_noarg() { + if [ "$FLAGSHIFT" -eq 2 ]; then + unset FLAGARG + FLAGSHIFT=1 + elif [ "${FLAGARG+x}" = x ]; then + # Means an argument was passed via equal sign as in -$FLAG=$FLAGARG + flag_errusage "flag $FLAGRAW does not accept an argument" + fi +} + +flag_errusage() { + caterr < but the release archive in + ~/.cache/d2/release will remain. + +--uninstall: + Uninstall the installed version of d2. The --method and --prefix flags must be the same + as for installation. i.e if you used --method standalone you must again use --method + standalone for uninstallation. With detect, the install script will try to use the OS + package manager to uninstall instead. + +All downloaded archives are cached into ~/.cache/d2/release. use \$XDG_CACHE_HOME to change +path of the cached assets. Release archives are unarchived into /usr/local/lib/d2/d2- + +note: Deleting the unarchived releases will cause --uninstall to stop working. + +You can rerun install.sh to update your version of D2. install.sh will avoid reinstalling +if the installed version is the latest unless --force is passed. +EOF +} + +main() { + if [ -n "${DEBUG-}" ]; then + set -x + fi + + METHOD=standalone + while :; do + flag_parse "$@" + case "$FLAG" in + h|help) + help + return 0 + ;; + dry-run) + flag_noarg && shift "$FLAGSHIFT" + DRY_RUN=1 + ;; + version) + flag_nonemptyarg && shift "$FLAGSHIFT" + VERSION=$FLAGARG + ;; + tala) + shift "$FLAGSHIFT" + TALA=${FLAGARG:-latest} + ;; + edge) + flag_noarg && shift "$FLAGSHIFT" + EDGE=1 + echoerr "$FLAGRAW is currently unimplemented" + return 1 + ;; + method) + flag_nonemptyarg && shift "$FLAGSHIFT" + METHOD=$FLAGARG + echoerr "$FLAGRAW is currently unimplemented" + return 1 + ;; + prefix) + flag_nonemptyarg && shift "$FLAGSHIFT" + export PREFIX=$FLAGARG + ;; + force) + flag_noarg && shift "$FLAGSHIFT" + FORCE=1 + ;; + uninstall) + flag_noarg && shift "$FLAGSHIFT" + UNINSTALL=1 + ;; + '') + shift "$FLAGSHIFT" + break + ;; + *) + flag_errusage "unrecognized flag $FLAGRAW" + ;; + esac + done + + if [ $# -gt 0 ]; then + flag_errusage "no arguments are accepted" + fi + + REPO=${REPO:-terrastruct/d2} + PREFIX=${PREFIX:-/usr/local} + OS=$(os) + ARCH=$(arch) + CACHE_DIR=$(cache_dir) + mkdir -p "$CACHE_DIR" + INSTALL_DIR=$PREFIX/lib/d2 + + if [ -n "${UNINSTALL-}" ]; then + uninstall + return 0 + fi + + VERSION=${VERSION:-latest} + if [ "$VERSION" = latest ]; then + header "fetching latest release info" + fetch_release_info + fi + + install +} + +install() { + install_d2 + if [ "${TALA-}" ]; then + # Run in subshell to avoid overwriting VERSION. + ( install_tala ) + fi + + COLOR=2 header success + log "d2-$VERSION-$OS-$ARCH has been successfully installed into $PREFIX" + if ! echo "$PATH" | grep -qF "$PREFIX/bin"; then + logcat >&2 </dev/null; then + INSTALLED_VERSION="$(d2 version)" + if [ ! "${FORCE-}" -a "$VERSION" = "$INSTALLED_VERSION" ]; then + log "skipping installation as version $VERSION is already installed." + return 0 + fi + log "uninstalling $INSTALLED_VERSION to install $VERSION" + if ! uninstall_d2; then + warn "failed to uninstall $INSTALLED_VERSION" + fi + fi + + header "installing d2-$VERSION" + install_standalone_d2 +} + +install_standalone_d2() { + ARCHIVE="d2-$VERSION-$OS-$ARCH.tar.gz" + log "installing standalone release $ARCHIVE from github" + + fetch_release_info + asset_line=$(cat "$RELEASE_INFO" | grep -n "$ARCHIVE" | cut -d: -f1 | head -n1) + asset_url=$(sed -n $((asset_line-3))p "$RELEASE_INFO" | sed 's/^.*: "\(.*\)",$/\1/g') + fetch_gh "$asset_url" "$CACHE_DIR/$ARCHIVE" 'application/octet-stream' + + sh_c="sh_c" + if ! is_prefix_writable; then + sh_c="sudo_sh_c" + fi + + "$sh_c" tar -C "$INSTALL_DIR" -xzf "$CACHE_DIR/$ARCHIVE" + "$sh_c" sh -c "'cd \"$INSTALL_DIR/d2-$VERSION\" && make install PREFIX=\"$PREFIX\"'" +} + +install_tala() { + install_standalone_tala +} + +install_standalone_tala() { + REPO="${REPO_TALA:-terrastruct/TALA}" + VERSION=$TALA + RELEASE_INFO= + fetch_release_info + + ARCHIVE="tala-$VERSION-$OS-$ARCH.tar.gz" + log "installing standalone release $ARCHIVE from github" + + asset_line=$(cat "$RELEASE_INFO" | grep -n "$ARCHIVE" | cut -d: -f1 | head -n1) + asset_url=$(sed -n $((asset_line-3))p "$RELEASE_INFO" | sed 's/^.*: "\(.*\)",$/\1/g') + + fetch_gh "$asset_url" "$CACHE_DIR/$ARCHIVE" 'application/octet-stream' + + sh_c="sh_c" + if ! is_prefix_writable; then + sh_c="sudo_sh_c" + fi + + "$sh_c" tar -C "$INSTALL_DIR" -xzf "$CACHE_DIR/$ARCHIVE" + "$sh_c" sh -c "'cd \"$INSTALL_DIR/tala-$VERSION\" && make install PREFIX=\"$PREFIX\"'" +} + +uninstall() { + if ! command -v d2 >/dev/null; then + echoerr "no version of d2 installed" + return 1 + fi + INSTALLED_VERSION="$(d2 --version)" + if ! uninstall_d2; then + echoerr "failed to uninstall $INSTALLED_VERSION" + return 1 + fi + if [ "${TALA-}" ]; then + if ! command -v d2plugin-tala >/dev/null; then + echoerr "no version of tala installed" + return 1 + fi + INSTALLED_VERSION="$(d2plugin-tala --version)" + if ! uninstall_tala; then + echoerr "failed to uninstall tala $INSTALLED_VERSION" + return 1 + fi + fi + return 0 +} + +uninstall_d2() { + header "uninstalling d2-$INSTALLED_VERSION" + uninstall_standalone_d2 +} + +uninstall_standalone_d2() { + log "uninstalling standalone release of d2-$INSTALLED_VERSION" + + if [ ! -e "$INSTALL_DIR/d2-$INSTALLED_VERSION" ]; then + echoerr "missing standalone install release directory $INSTALL_DIR/d2-$INSTALLED_VERSION" + return 1 + fi + + sh_c="sh_c" + if ! is_prefix_writable; then + sh_c="sudo_sh_c" + fi + + "$sh_c" sh -c "'cd \"$INSTALL_DIR/d2-$INSTALLED_VERSION\" && make uninstall PREFIX=\"$PREFIX\"'" + "$sh_c" rm -rf "$INSTALL_DIR/d2-$INSTALLED_VERSION" +} + +uninstall_tala() { + uninstall_standalone_tala +} + +uninstall_standalone_tala() { + log "uninstalling standalone release tala-$INSTALLED_VERSION" + + if [ ! -e "$INSTALL_DIR/tala-$INSTALLED_VERSION" ]; then + echoerr "missing standalone install release directory $INSTALL_DIR/tala-$INSTALLED_VERSION" + return 1 + fi + + sh_c="sh_c" + if ! is_prefix_writable; then + sh_c="sudo_sh_c" + fi + + "$sh_c" sh -c "'cd \"$INSTALL_DIR/tala-$INSTALLED_VERSION\" && make uninstall PREFIX=\"$PREFIX\"'" + "$sh_c" rm -rf "$INSTALL_DIR/tala-$INSTALLED_VERSION" +} + +is_prefix_writable() { + sh_c "mkdir -p '$INSTALL_DIR' 2>/dev/null" || true + # The reason for checking whether bin is writable specifically is that on macOS you have + # /usr/local owned by root but you don't need root to write to its subdirectories which + # is all we want to do. + if [ ! -w "$PREFIX/bin" ]; then + return 0 + fi +} + +cache_dir() { + if [ -n "${XDG_CACHE_HOME-}" ]; then + echo "$XDG_CACHE_HOME/d2/release" + elif [ -n "${HOME-}" ]; then + echo "$HOME/.cache/d2/release" + else + echo "/tmp/d2-cache/release" + fi +} + +fetch_release_info() { + if [ -n "${RELEASE_INFO-}" ]; then + return 0 + fi + + log "fetching info on $VERSION version of $REPO" + RELEASE_INFO=$(mktemp -d)/release-info.json + if [ "$VERSION" = latest ]; then + release_info_url="https://api.github.com/repos/$REPO/releases/$VERSION" + else + release_info_url="https://api.github.com/repos/$REPO/releases/tags/$VERSION" + fi + fetch_gh "$release_info_url" "$RELEASE_INFO" \ + 'application/json' + VERSION=$(cat "$RELEASE_INFO" | grep -m1 tag_name | sed 's/^.*: "\(.*\)",$/\1/g') +} + +curl_gh() { + sh_c curl -fL ${GITHUB_TOKEN+"-H \"Authorization: Bearer \$GITHUB_TOKEN\""} "$@" +} + +fetch_gh() { + url=$1 + file=$2 + accept=$3 + + if [ -e "$file" ]; then + log "reusing $file" + return + fi + + curl_gh -#o "$file.inprogress" -C- -H "'Accept: $accept'" "$url" + sh_c mv "$file.inprogress" "$file" +} + +main "$@" diff --git a/lib/version/version.go b/lib/version/version.go index ef7703a06..65db5ca1c 100644 --- a/lib/version/version.go +++ b/lib/version/version.go @@ -2,6 +2,7 @@ package version import ( "context" + "fmt" "github.com/google/go-github/github" "oss.terrastruct.com/cmdlog" @@ -11,12 +12,17 @@ import ( var Version = "master (built from source)" func CheckVersion(ctx context.Context, logger *cmdlog.Logger) { - logger.Info.Printf("D2 version: %s\n", Version) + fmt.Println(Version) if Version == "master (built from source)" { return } + // Install script uses -v to check the version, we shouldn't be checking for + // updates here... + // https://github.com/terrastruct/d2/issues/49#issuecomment-1313229683 + return + logger.Info.Printf("Checking for updates...") latest, err := getLatestVersion(ctx) if err != nil {