diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 73f3d930c..a18a4bd80 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,7 +11,7 @@ jobs: - uses: actions/checkout@v3 with: submodules: recursive - - run: TERM=xterm-256color ./make.sh assert-linear + - run: COLOR=1 ./make.sh assert-linear env: GITHUB_TOKEN: ${{ secrets._GITHUB_TOKEN }} DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_WEBHOOK_URL }} @@ -25,7 +25,21 @@ jobs: with: go-version-file: ./go.mod cache: true - - run: TERM=xterm-256color ./make.sh fmt + - run: COLOR=1 ./make.sh fmt + env: + GITHUB_TOKEN: ${{ secrets._GITHUB_TOKEN }} + DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_WEBHOOK_URL }} + gen: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + with: + submodules: recursive + - uses: actions/setup-go@v3 + with: + go-version-file: ./go.mod + cache: true + - run: COLOR=1 ./make.sh gen env: GITHUB_TOKEN: ${{ secrets._GITHUB_TOKEN }} DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_WEBHOOK_URL }} @@ -39,7 +53,7 @@ jobs: with: go-version-file: ./go.mod cache: true - - run: TERM=xterm-256color ./make.sh lint + - run: COLOR=1 ./make.sh lint env: GITHUB_TOKEN: ${{ secrets._GITHUB_TOKEN }} DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_WEBHOOK_URL }} @@ -53,7 +67,7 @@ jobs: with: go-version-file: ./go.mod cache: true - - run: TERM=xterm-256color ./make.sh build + - run: COLOR=1 ./make.sh build env: GITHUB_TOKEN: ${{ secrets._GITHUB_TOKEN }} DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_WEBHOOK_URL }} @@ -67,7 +81,7 @@ jobs: with: go-version-file: ./go.mod cache: true - - run: TERM=xterm-256color ./make.sh test + - run: COLOR=1 ./make.sh test env: GITHUB_TOKEN: ${{ secrets._GITHUB_TOKEN }} DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_WEBHOOK_URL }} @@ -86,7 +100,7 @@ jobs: with: go-version-file: ./go.mod cache: true - - run: TERM=xterm-256color ./make.sh race + - run: COLOR=1 ./make.sh race env: GITHUB_TOKEN: ${{ secrets._GITHUB_TOKEN }} DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_WEBHOOK_URL }} diff --git a/.github/workflows/daily.yml b/.github/workflows/daily.yml index ed2e89be5..6334923c1 100644 --- a/.github/workflows/daily.yml +++ b/.github/workflows/daily.yml @@ -20,7 +20,7 @@ jobs: with: go-version-file: ./go.mod cache: true - - run: CI_ALL=1 TERM=xterm-256color ./make.sh + - run: CI_ALL=1 COLOR=1 ./make.sh env: GITHUB_TOKEN: ${{ secrets._GITHUB_TOKEN }} DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_WEBHOOK_URL }} diff --git a/Makefile b/Makefile index 46577c730..9780b7bc0 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ .POSIX: .PHONY: all -all: fmt lint build test +all: fmt gen lint build test ifdef CI all: assert-linear endif @@ -9,6 +9,9 @@ endif .PHONY: fmt fmt: prefix "$@" ./ci/sub/fmt/make.sh +.PHONY: gen +gen: + prefix "$@" ./ci/gen.sh .PHONY: lint lint: prefix "$@" go vet --composites=false ./... diff --git a/README.md b/README.md index df9cd9564..14fa751fa 100644 --- a/README.md +++ b/README.md @@ -22,8 +22,6 @@ - [Quickstart](#quickstart) - [Install](#install) - * [Install script](#install-script) - * [Install from source](#install-from-source) - [D2 as a library](#d2-as-a-library) - [Themes](#themes) - [Fonts](#fonts) @@ -58,42 +56,20 @@ A browser window will open with `out.svg` and live-reload on changes to `in.d2`. ## Install -### Install script - -The recommended way to install is to run our install script, which will figure out the -best way to install based on your machine. +The easiest way to install is with our install script: ```sh -# With --dry-run 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 -- --dry-run -# If things look good, install for real. curl -fsSL https://d2lang.com/install.sh | sh -s -- ``` -We have precompiled binaries on the [releases](https://github.com/terrastruct/d2/releases) -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. - To uninstall: ```sh -curl -fsSL https://d2lang.com/install.sh | sh -s -- --uninstall --dry-run -# If things look good, uninstall 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. - -### Install from source - -Alternatively, you can install from source: - -```sh -go install oss.terrastruct.com/d2 -``` +For detailed installation docs, with alternative methods and examples for each OS, see +[./docs/INSTALL.md](./docs/INSTALL.md). ## D2 as a library diff --git a/ci/gen.sh b/ci/gen.sh new file mode 100755 index 000000000..db0c557ab --- /dev/null +++ b/ci/gen.sh @@ -0,0 +1,11 @@ +#!/bin/sh +set -eu +cd -- "$(dirname "$0")/.." +. ./ci/sub/lib.sh + +./ci/release/gen_install.sh +./ci/release/gen_template_lib.sh + +if [ -n "${CI-}" ]; then + git_assert_clean +fi diff --git a/ci/release/_install.sh b/ci/release/_install.sh index 2917323ad..4cd840ca9 100755 --- a/ci/release/_install.sh +++ b/ci/release/_install.sh @@ -18,8 +18,11 @@ usage: $arg0 [--dry-run] [--version vX.X.X] [--edge] [--method detect] [--prefix [--tala latest] [--force] [--uninstall] install.sh automates the installation of D2 onto your system. It currently only supports -the installation of standalone releases from GitHub. If you pass --edge, it will clone the -source, build a release and install from it. +the installation of standalone releases from GitHub and via Homebrew on macOS. See the +docs for --detect below for more information + +If you pass --edge, it will clone the source, build a release and install from it. +--edge is incompatible with --tala and currently unimplemented. Flags: @@ -29,6 +32,8 @@ Flags: --version vX.X.X Pass to have install.sh install the given version instead of the latest version. + warn: The version may not be obeyed with package manager installations. Use + --method=standalone to enforce the version. --edge Pass to build and install D2 from source. This will still use --method if set to detect @@ -36,14 +41,15 @@ Flags: if an unsupported package manager is used. To install from source like a dev would, use go install oss.terrastruct.com/d2 note: currently unimplemented. + warn: incompatible with --tala as TALA is closed source. ---method [detect | standalone] +--method [detect | standalone | homebrew ] Pass to control the method by which to install. Right now we only support standalone releases from GitHub but later we'll add support for brew, rpm, deb and more. - note: currently unimplemented. - - detect is currently unimplemented but would use your OS's package manager - automatically. + - detect will use your OS's package manager automatically. + So far it only detects macOS and automatically uses homebrew. + - homebrew uses https://brew.sh/ which is a macOS and Linux package manager. - standalone installs a standalone release archive into the unix hierarchy path specified by --prefix which defaults to /usr/local Ensure /usr/local/bin is in your \$PATH to use it. @@ -51,16 +57,19 @@ Flags: --prefix /usr/local Controls the unix hierarchy path into which standalone releases are installed. Defaults to /usr/local. You may also want to use ~/.local to avoid needing sudo. - Remember that whatever you use, you must have the bin directory of your prefix - path in \$PATH to execute the d2 binary. For example, if my prefix directory is + We use ~/.local by default on arm64 macOS machines as SIP now disables access to + /usr/local. Remember that whatever you use, you must have the bin directory of your + prefix path in \$PATH to execute the d2 binary. For example, if my prefix directory is /usr/local then my \$PATH must contain /usr/local/bin. --tala [latest] Install Terrastruct's closed source TALA for improved layouts. - See https://github.com/terrastruct/TALA + See https://github.com/terrastruct/tala It optionally takes an argument of the TALA version to install. Installation obeys all other flags, just like the installation of d2. For example, the d2plugin-tala binary will be installed into /usr/local/bin/d2plugin-tala + warn: The version may not be obeyed with package manager installations. Use + --method=standalone to enforce the version. --force: Force installation over the existing version even if they match. It will attempt a @@ -73,6 +82,7 @@ Flags: 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. + note: tala will also be uninstalled if installed. 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- @@ -85,9 +95,7 @@ EOF } main() { - METHOD=standalone - while :; do - flag_parse "$@" + while flag_parse "$@"; do case "$FLAG" in h|help) help @@ -114,8 +122,6 @@ main() { method) flag_nonemptyarg && shift "$FLAGSHIFT" METHOD=$FLAGARG - echoerr "$FLAGRAW is currently unimplemented" - return 1 ;; prefix) flag_nonemptyarg && shift "$FLAGSHIFT" @@ -129,15 +135,12 @@ main() { flag_noarg && shift "$FLAGSHIFT" UNINSTALL=1 ;; - '') - shift "$FLAGSHIFT" - break - ;; *) flag_errusage "unrecognized flag $FLAGRAW" ;; esac done + shift "$FLAGSHIFT" if [ $# -gt 0 ]; then flag_errusage "no arguments are accepted" @@ -153,49 +156,86 @@ main() { PREFIX=${PREFIX:-/usr/local} CACHE_DIR=$(cache_dir) mkdir -p "$CACHE_DIR" + METHOD=${METHOD:-detect} INSTALL_DIR=$PREFIX/lib/d2 + case $METHOD in + detect) + case "$OS" in + macos) + if command -v brew >/dev/null; then + log "detected macOS with homebrew, using homebrew for (un)installation" + METHOD=homebrew + else + warn "detected macOS without homebrew, falling back to --method=standalone" + METHOD=standalone + fi + ;; + *) + warn "unrecognized OS $OS, falling back to --method=standalone" + METHOD=standalone + ;; + esac + ;; + standalone) ;; + homebrew) ;; + *) + echoerr "unknown (un)installation method $METHOD" + return 1 + ;; + esac + if [ -n "${UNINSTALL-}" ]; then uninstall - return 0 + if [ -n "${DRY_RUN-}" ]; then + log "Rerun without --dry-run to execute printed commands and perform uninstall." + fi + else + install + if [ -n "${DRY_RUN-}" ]; then + log "Rerun without --dry-run to execute printed commands and perform install." + fi fi - - VERSION=${VERSION:-latest} - if [ "$VERSION" = latest ]; then - header "fetching latest release info" - fetch_release_info - fi - - install } install() { - install_d2 - if [ -n "${TALA-}" ]; then - # Run in subshell to avoid overwriting VERSION. - TALA_VERSION="$( install_tala && echo "$VERSION" )" - fi + case $METHOD in + standalone) + install_d2_standalone + if [ -n "${TALA-}" ]; then + # Run in subshell to avoid overwriting VERSION. + TALA_VERSION="$( RELEASE_INFO= install_tala_standalone && echo "$VERSION" )" + fi + ;; + homebrew) + install_d2_brew + if [ -n "${TALA-}" ]; then install_tala_brew; fi + ;; + esac - COLOR=2 header success + FGCOLOR=2 bigheader 'next steps' + case $METHOD in + standalone) install_post_standalone ;; + homebrew) install_post_brew ;; + esac +} + +install_post_standalone() { log "d2-$VERSION-$OS-$ARCH has been successfully installed into $PREFIX" if [ -n "${TALA-}" ]; then log "tala-$TALA_VERSION-$OS-$ARCH has been successfully installed into $PREFIX" fi - log "Rerun this install script with --uninstall to uninstall" + log "Rerun this install script with --uninstall to uninstall." + log if ! echo "$PATH" | grep -qF "$PREFIX/bin"; then logcat >&2 <&2 <&2 <&2 </dev/null; then INSTALLED_VERSION="$(d2 version)" if [ ! "${FORCE-}" -a "$VERSION" = "$INSTALLED_VERSION" ]; then - log "skipping installation as version $VERSION is already installed." + log "skipping installation as d2 $VERSION is already installed." return 0 fi - log "uninstalling $INSTALLED_VERSION to install $VERSION" - if ! uninstall_d2; then - warn "failed to uninstall $INSTALLED_VERSION" + log "uninstalling d2 $INSTALLED_VERSION to install $VERSION" + if ! uninstall_d2_standalone; then + warn "failed to uninstall d2 $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" @@ -256,19 +321,38 @@ install_standalone_d2() { "$sh_c" sh -c "'cd \"$INSTALL_DIR/d2-$VERSION\" && make install PREFIX=\"$PREFIX\"'" } -install_tala() { - REPO="${REPO_TALA:-terrastruct/TALA}" - VERSION=$TALA - RELEASE_INFO= - fetch_release_info - header "installing tala-$VERSION" - install_standalone_tala +install_d2_brew() { + header "installing d2 with homebrew" + sh_c brew tap terrastruct/d2 + sh_c brew install d2 } -install_standalone_tala() { +install_tala_standalone() { + REPO="${REPO_TALA:-terrastruct/tala}" + VERSION=$TALA + + header "installing tala-$VERSION" + + if [ "$VERSION" = latest ]; then + fetch_release_info + fi + + if command -v d2plugin-tala >/dev/null; then + INSTALLED_VERSION="$(d2plugin-tala --version)" + if [ ! "${FORCE-}" -a "$VERSION" = "$INSTALLED_VERSION" ]; then + log "skipping installation as tala $VERSION is already installed." + return 0 + fi + log "uninstalling tala $INSTALLED_VERSION to install $VERSION" + if ! uninstall_tala_standalone; then + warn "failed to uninstall tala $INSTALLED_VERSION" + fi + fi + ARCHIVE="tala-$VERSION-$OS-$ARCH.tar.gz" log "installing standalone release $ARCHIVE from github" + fetch_release_info asset_line=$(sh_c 'cat "$RELEASE_INFO" | grep -n "$ARCHIVE" | cut -d: -f1 | head -n1') asset_url=$(sh_c 'sed -n $((asset_line-3))p "$RELEASE_INFO" | sed "s/^.*: \"\(.*\)\",$/\1/g"') @@ -284,36 +368,40 @@ install_standalone_tala() { "$sh_c" sh -c "'cd \"$INSTALL_DIR/tala-$VERSION\" && make install PREFIX=\"$PREFIX\"'" } +install_tala_brew() { + header "installing tala with homebrew" + sh_c brew tap terrastruct/d2 + sh_c brew install tala +} + uninstall() { + # We uninstall tala first as package managers require that it be uninstalled before + # uninstalling d2 as TALA depends on d2. + if command -v d2plugin-tala >/dev/null; then + INSTALLED_VERSION="$(d2plugin-tala --version)" + header "uninstalling tala-$INSTALLED_VERSION" + case $METHOD in + standalone) uninstall_tala_standalone ;; + homebrew) uninstall_tala_brew ;; + esac + elif [ "${TALA-}" ]; then + warn "no version of tala installed" + fi + if ! command -v d2 >/dev/null; then warn "no version of d2 installed" return 0 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 - warn "no version of tala installed" - return 0 - 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 + case $METHOD in + standalone) uninstall_d2_standalone ;; + homebrew) uninstall_d2_brew ;; + esac } -uninstall_standalone_d2() { +uninstall_d2_standalone() { log "uninstalling standalone release of d2-$INSTALLED_VERSION" if [ ! -e "$INSTALL_DIR/d2-$INSTALLED_VERSION" ]; then @@ -331,12 +419,11 @@ uninstall_standalone_d2() { "$sh_c" rm -rf "$INSTALL_DIR/d2-$INSTALLED_VERSION" } -uninstall_tala() { - header "uninstalling tala-$INSTALLED_VERSION" - uninstall_standalone_tala +uninstall_d2_brew() { + sh_c brew remove d2 } -uninstall_standalone_tala() { +uninstall_tala_standalone() { log "uninstalling standalone release tala-$INSTALLED_VERSION" if [ ! -e "$INSTALL_DIR/tala-$INSTALLED_VERSION" ]; then @@ -354,6 +441,10 @@ uninstall_standalone_tala() { "$sh_c" rm -rf "$INSTALL_DIR/tala-$INSTALLED_VERSION" } +uninstall_tala_brew() { + sh_c brew remove tala +} + is_prefix_writable() { sh_c "mkdir -p '$INSTALL_DIR' 2>/dev/null" || true # The reason for checking whether $INSTALL_DIR is writable is that on macOS you have @@ -409,4 +500,9 @@ fetch_gh() { sh_c mv "$file.inprogress" "$file" } +brew() { + # Makes brew sane. + HOMEBREW_NO_INSTALL_CLEANUP=1 HOMEBREW_NO_AUTO_UPDATE=1 command brew "$@" +} + main "$@" diff --git a/ci/release/build.sh b/ci/release/build.sh index 135160f82..ce2e4eaf4 100755 --- a/ci/release/build.sh +++ b/ci/release/build.sh @@ -42,8 +42,7 @@ EOF } main() { - while :; do - flag_parse "$@" + while flag_parse "$@"; do case "$FLAG" in h|help) help @@ -78,16 +77,12 @@ main() { flag_noarg && shift "$FLAGSHIFT" LOCKFILE_FORCE=1 ;; - '') - shift "$FLAGSHIFT" - break - ;; *) flag_errusage "unrecognized flag $FLAGRAW" ;; esac done - + shift "$FLAGSHIFT" if [ $# -gt 0 ]; then flag_errusage "no arguments are accepted" fi @@ -169,16 +164,16 @@ build_local() { build_remote_macos() { sh_c lockfile_ssh "$REMOTE_HOST" .d2-build-lock - trap unlockfile_ssh EXIT sh_c ssh "$REMOTE_HOST" mkdir -p src sh_c rsync --archive --human-readable --delete ./ "$REMOTE_HOST:src/d2/" - sh_c ssh "$REMOTE_HOST" "DRY_RUN=${DRY_RUN-} \ + sh_c ssh "$REMOTE_HOST" "COLOR=${COLOR-} \ +TERM=${TERM-} \ +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:/opt/homebrew/bin:/opt/homebrew/sbin\\\${PATH+:\\\$PATH}\\\" \ ./src/d2/ci/release/_build.sh" sh_c mkdir -p "$HW_BUILD_DIR" @@ -187,16 +182,16 @@ PATH=\\\"/usr/local/bin:/usr/local/sbin:/opt/homebrew/bin:/opt/homebrew/sbin\\\$ build_remote_linux() { sh_c lockfile_ssh "$REMOTE_HOST" .d2-build-lock - trap unlockfile_ssh EXIT sh_c ssh "$REMOTE_HOST" mkdir -p src sh_c rsync --archive --human-readable --delete ./ "$REMOTE_HOST:src/d2/" - sh_c ssh "$REMOTE_HOST" "DRY_RUN=${DRY_RUN-} \ + sh_c ssh "$REMOTE_HOST" "COLOR=${COLOR-} \ +TERM=${TERM-} \ +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 mkdir -p "$HW_BUILD_DIR" sh_c rsync --archive --human-readable "$REMOTE_HOST:src/d2/$ARCHIVE" "$ARCHIVE" diff --git a/ci/release/build_docker.sh b/ci/release/build_docker.sh index 7c3751af2..2939c5dd0 100755 --- a/ci/release/build_docker.sh +++ b/ci/release/build_docker.sh @@ -13,5 +13,4 @@ docker_run \ -e OS \ -e ARCH \ -e ARCHIVE \ - -e TERM \ "$tag" ./src/d2/ci/release/_build.sh diff --git a/ci/release/builders/aws_copy_keys.sh b/ci/release/builders/aws_copy_keys.sh index df5c46408..cc0d548ef 100755 --- a/ci/release/builders/aws_copy_keys.sh +++ b/ci/release/builders/aws_copy_keys.sh @@ -12,8 +12,7 @@ EOF } main() { - while :; do - flag_parse "$@" + while flag_parse "$@"; do case "$FLAG" in h|help) help @@ -27,15 +26,12 @@ main() { flag_nonemptyarg && shift "$FLAGSHIFT" KEY_FILE=$FLAGARG ;; - '') - shift "$FLAGSHIFT" - break - ;; *) flag_errusage "unrecognized flag $FLAGRAW" ;; esac done + shift "$FLAGSHIFT" if [ -z "${KEY_FILE-}" ]; then echoerr "-i is required" exit 1 diff --git a/ci/release/builders/aws_ensure.sh b/ci/release/builders/aws_ensure.sh index 3a875d832..bfb630ed0 100755 --- a/ci/release/builders/aws_ensure.sh +++ b/ci/release/builders/aws_ensure.sh @@ -12,8 +12,7 @@ EOF } main() { - while :; do - flag_parse "$@" + while flag_parse "$@"; do case "$FLAG" in h|help) help @@ -27,15 +26,12 @@ main() { flag_noarg && shift "$FLAGSHIFT" SKIP_CREATE=1 ;; - '') - shift "$FLAGSHIFT" - break - ;; *) flag_errusage "unrecognized flag $FLAGRAW" ;; esac done + shift "$FLAGSHIFT" if [ $# -gt 0 ]; then flag_errusage "no arguments are accepted" fi @@ -204,7 +200,7 @@ init_remote_hosts() { header macos-arm64 REMOTE_HOST=$TSTRUCT_MACOS_ARM64_BUILDER init_remote_macos - COLOR=2 header summary + FGCOLOR=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" diff --git a/ci/release/builders/ssh.sh b/ci/release/builders/ssh.sh index 0325a99dd..3d0e7e995 100755 --- a/ci/release/builders/ssh.sh +++ b/ci/release/builders/ssh.sh @@ -12,8 +12,7 @@ EOF } main() { - while :; do - flag_parse "$@" + while flag_parse "$@"; do case "$FLAG" in h|help) help @@ -27,15 +26,12 @@ main() { flag_reqarg && shift "$FLAGSHIFT" JOBFILTER="$FLAGARG" ;; - '') - shift "$FLAGSHIFT" - break - ;; *) flag_errusage "unrecognized flag $FLAGRAW" ;; esac done + shift "$FLAGSHIFT" REMOTE_HOST=$TSTRUCT_LINUX_AMD64_BUILDER; runjob linux-amd64 ssh "$REMOTE_HOST" "$@" REMOTE_HOST=$TSTRUCT_LINUX_ARM64_BUILDER; runjob linux-arm64 ssh "$REMOTE_HOST" "$@" diff --git a/ci/release/changelogs/next.md b/ci/release/changelogs/next.md index d12aa07e4..1fab4e6f8 100644 --- a/ci/release/changelogs/next.md +++ b/ci/release/changelogs/next.md @@ -7,7 +7,8 @@ For v0.0.99 we focused on X, Y and Z. Enjoy! #### Improvements 🔧 -- Add table columns indices in edges between SQL Tables so that layout engines can route exactly between them +- Equivalency between flags and environment variables. You can set either one for all + options (flags take precedence). #### Bugfixes 🔴 diff --git a/ci/release/gen_template_lib.sh b/ci/release/gen_template_lib.sh new file mode 100755 index 000000000..1f523767a --- /dev/null +++ b/ci/release/gen_template_lib.sh @@ -0,0 +1,29 @@ +#!/bin/sh +set -eu +cd -- "$(dirname "$0")/../.." +. ./ci/sub/lib.sh + +sh_c chmod +w ./ci/release/template/scripts/lib.sh +sh_c cat >./ci/release/template/scripts/lib.sh <\>./ci/release/template/scripts/lib.sh +sh_c chmod -w ./ci/release/template/scripts/lib.sh diff --git a/ci/release/template/man/d2.1 b/ci/release/template/man/d2.1 index faf28e1fa..4a5ca14fc 100644 --- a/ci/release/template/man/d2.1 +++ b/ci/release/template/man/d2.1 @@ -40,10 +40,22 @@ See more docs, the source code and license at .It Fl w , -watch Ar false Watch for changes to input and live reload. Use .Ev $PORT and Ev $HOST to specify the listening address. -.Ev $D2_PORT and $D2_HOST are also accepted and take priority. Default is localhost:0 +.It Fl h , -host Ar localhost +Host listening address when used with +.Ar watch +.Ns . +.It Fl p , -port Ar 0 +Port listening address when used with +.Ar watch +.Ns . .It Fl t , -theme Ar 0 Set the diagram theme to the passed integer. For a list of available options, see .Lk https://oss.terrastruct.com/d2 +.Ns . +.It Fl l , -layout Ar dagre +Set the diagram layout engine to the passed string. For a list of available options, run +.Ar layout +.Ns . .It Fl b , -bundle Ar true Bundle all assets and layers into the output svg. .It Fl d , -debug diff --git a/ci/release/template/scripts/lib.sh b/ci/release/template/scripts/lib.sh index 0a232eb74..61f7c1138 100644 --- a/ci/release/template/scripts/lib.sh +++ b/ci/release/template/scripts/lib.sh @@ -1,8 +1,21 @@ #!/bin/sh -if [ -n "${DEBUG-}" ]; then - set -x +# ************* +# DO NOT EDIT +# +# lib.sh was bundled together from +# +# - ./ci/sub/lib/rand.sh +# - ./ci/sub/lib/log.sh +# +# Generated by ./ci/release/gen_template_lib.sh. +# ************* + +#!/bin/sh +if [ "${LIB_RAND-}" ]; then + return 0 fi +LIB_RAND=1 rand() { seed="$1" @@ -14,15 +27,51 @@ rand() { } pick() { + if ! command -v shuf >/dev/null || ! command -v md5sum >/dev/null; then + eval "_echo \"\$3\"" + return + fi + seed="$1" shift i="$(rand "$seed" "1-$#")" eval "_echo \"\$$i\"" } +#!/bin/sh +if [ "${LIB_LOG-}" ]; then + return 0 +fi +LIB_LOG=1 + +if [ -n "${DEBUG-}" ]; then + set -x +fi tput() { - if [ -n "$TERM" ]; then - command tput "$@" + if should_color; then + TERM=${TERM:-xterm-256color} command tput "$@" + fi +} + +should_color() { + if [ -n "${COLOR-}" ]; then + if [ "$COLOR" = 0 -o "$COLOR" = false ]; then + _COLOR= + return 1 + elif [ "$COLOR" = 1 -o "$COLOR" = true ]; then + _COLOR=1 + return 0 + else + printf '$COLOR must be 0, 1, false or true but got %s' "$COLOR" >&2 + fi + fi + + if [ -t 1 ]; then + _COLOR=1 + return 0 + else + _COLOR= + return 1 fi } @@ -59,14 +108,14 @@ printfp() {( prefix="$1" shift - if [ -z "${COLOR:-}" ]; then - COLOR="$(get_rand_color "$prefix")" + if [ -z "${FGCOLOR-}" ]; then + FGCOLOR="$(get_rand_color "$prefix")" fi - printf '%s' "$(setaf "$COLOR" "$prefix")" - - if [ $# -gt 0 ]; then - printf ': ' - printf "$@" + should_color || true + if [ $# -eq 0 ]; then + printf '%s' "$(COLOR=${_COLOR-} setaf "$FGCOLOR" "$prefix")" + else + printf '%s: %s\n' "$(COLOR=${_COLOR-} setaf "$FGCOLOR" "$prefix")" "$(printf "$@")" fi )} @@ -74,13 +123,8 @@ catp() { prefix="$1" shift - printfp "$prefix" - printf ': ' - read -r line - _echo "$line" - - indent=$(repeat ' ' 2) - sed "s/^/$indent/" + should_color || true + sed "s/^/$(COLOR=${_COLOR-} printfp "$prefix" '')/" } repeat() { @@ -94,48 +138,150 @@ strlen() { } echoerr() { - COLOR=1 echop err "$*" >&2 + FGCOLOR=1 logp err "$*" | humanpath>&2 } caterr() { - COLOR=1 catp err "$@" >&2 + FGCOLOR=1 logpcat err "$@" | humanpath >&2 } printferr() { - COLOR=1 printfp err "$@" >&2 + FGCOLOR=1 logfp err "$@" | humanpath >&2 } logp() { - echop "$@" >&2 + should_color >&2 || true + COLOR=${_COLOR-} echop "$@" | humanpath >&2 } logfp() { - printfp "$@" >&2 + should_color >&2 || true + COLOR=${_COLOR-} printfp "$@" | humanpath >&2 } logpcat() { - catp "$@" >&2 + should_color >&2 || true + COLOR=${_COLOR-} catp "$@" | humanpath >&2 } log() { - COLOR=5 logp log "$@" + FGCOLOR=5 logp log "$@" } logf() { - COLOR=5 logfp log "$@" + FGCOLOR=5 logfp log "$@" } logcat() { - COLOR=5 catp log "$@" >&2 + FGCOLOR=5 logpcat log "$@" +} + +warn() { + FGCOLOR=3 logp warn "$@" +} + +warnf() { + FGCOLOR=3 logfp warn "$@" +} + +warncat() { + FGCOLOR=3 logpcat warn "$@" } sh_c() { - COLOR=3 logp exec "$*" + FGCOLOR=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 + return "$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 +} diff --git a/ci/sub b/ci/sub index df51b9089..824046d95 160000 --- a/ci/sub +++ b/ci/sub @@ -1 +1 @@ -Subproject commit df51b90892737ebe9feca3dd982bcdfc7f684834 +Subproject commit 824046d952b1442c76a057553591652c889fb7cb diff --git a/cmd/d2/help.go b/cmd/d2/help.go index 6531f1379..33ac3ad16 100644 --- a/cmd/d2/help.go +++ b/cmd/d2/help.go @@ -28,13 +28,13 @@ Subcommands: %[1]s layout [layout name] - Display long help for a particular layout engine See more docs and the source code at https://oss.terrastruct.com/d2 -`, ms.Name, ms.FlagHelp()) +`, ms.Name, ms.Opts.Defaults()) } func layoutHelp(ctx context.Context, ms *xmain.State) error { - if len(ms.FlagSet.Args()) == 1 { + if len(ms.Opts.Flags.Args()) == 1 { return shortLayoutHelp(ctx, ms) - } else if len(ms.FlagSet.Args()) == 2 { + } else if len(ms.Opts.Flags.Args()) == 2 { return longLayoutHelp(ctx, ms) } else { return pluginSubcommand(ctx, ms) @@ -61,7 +61,7 @@ func shortLayoutHelp(ctx context.Context, ms *xmain.State) error { %s Usage: - To use a particular layout engine, set the environment variable D2_LAYOUT=[layout name]. + To use a particular layout engine, set the environment variable D2_LAYOUT=[name] or flag --layout=[name]. Example: D2_LAYOUT=dagre d2 in.d2 out.svg @@ -75,7 +75,7 @@ See more docs at https://oss.terrastruct.com/d2 } func longLayoutHelp(ctx context.Context, ms *xmain.State) error { - layout := ms.FlagSet.Arg(1) + layout := ms.Opts.Flags.Arg(1) plugin, path, err := d2plugin.FindPlugin(ctx, layout) if errors.Is(err, exec.ErrNotFound) { return layoutNotFound(ctx, layout) @@ -119,13 +119,13 @@ For more information on setup, please visit https://github.com/terrastruct/d2.`, } func pluginSubcommand(ctx context.Context, ms *xmain.State) error { - layout := ms.FlagSet.Arg(1) + layout := ms.Opts.Flags.Arg(1) plugin, _, err := d2plugin.FindPlugin(ctx, layout) if errors.Is(err, exec.ErrNotFound) { return layoutNotFound(ctx, layout) } - ms.Args = ms.FlagSet.Args()[2:] + ms.Opts.Args = ms.Opts.Flags.Args()[2:] return d2plugin.Serve(plugin)(ctx, ms) } diff --git a/cmd/d2/main.go b/cmd/d2/main.go index fb21c1bfa..e18f5ce76 100644 --- a/cmd/d2/main.go +++ b/cmd/d2/main.go @@ -6,7 +6,6 @@ import ( "fmt" "os/exec" "path/filepath" - "strconv" "strings" "time" @@ -32,19 +31,38 @@ func run(ctx context.Context, ms *xmain.State) (err error) { // :( ctx = xmain.DiscardSlog(ctx) - watchFlag := ms.FlagSet.BoolP("watch", "w", false, "watch for changes to input and live reload. Use $PORT and $HOST to specify the listening address.\n$D2_PORT and $D2_HOST are also accepted and take priority. Default is localhost:0") - themeFlag := ms.FlagSet.Int64P("theme", "t", 0, "set the diagram theme. For a list of available options, see https://oss.terrastruct.com/d2") - bundleFlag := ms.FlagSet.BoolP("bundle", "b", true, "when outputting SVG, bundle all assets and layers into the output file") - versionFlag := ms.FlagSet.BoolP("version", "v", false, "get the version") - debugFlag := ms.FlagSet.BoolP("debug", "d", false, "print debug logs") - err = ms.FlagSet.Parse(ms.Args) + // These should be kept up-to-date with the d2 man page + watchFlag, err := ms.Opts.Bool("D2_WATCH", "watch", "w", false, "watch for changes to input and live reload. Use $HOST and $PORT to specify the listening address.\n(default localhost:0, which is will open on a randomly available local port).") + if err != nil { + return xmain.UsageErrorf(err.Error()) + } + hostFlag := ms.Opts.String("HOST", "host", "h", "localhost", "host listening address when used with watch") + portFlag := ms.Opts.String("PORT", "port", "p", "0", "port listening address when used with watch") + bundleFlag, err := ms.Opts.Bool("D2_BUNDLE", "bundle", "b", true, "when outputting SVG, bundle all assets and layers into the output file.") + if err != nil { + return xmain.UsageErrorf(err.Error()) + } + debugFlag, err := ms.Opts.Bool("DEBUG", "debug", "d", false, "print debug logs.") + if err != nil { + return xmain.UsageErrorf(err.Error()) + } + layoutFlag := ms.Opts.String("D2_LAYOUT", "layout", "l", "dagre", `the layout engine used.`) + themeFlag, err := ms.Opts.Int64("D2_THEME", "theme", "t", 0, "the diagram theme ID. For a list of available options, see https://oss.terrastruct.com/d2") + if err != nil { + return xmain.UsageErrorf(err.Error()) + } + versionFlag, err := ms.Opts.Bool("", "version", "v", false, "get the version") + if err != nil { + return xmain.UsageErrorf(err.Error()) + } + err = ms.Opts.Flags.Parse(ms.Opts.Args) if !errors.Is(err, pflag.ErrHelp) && err != nil { return xmain.UsageErrorf("failed to parse flags: %v", err) } - if len(ms.FlagSet.Args()) > 0 { - switch ms.FlagSet.Arg(0) { + if len(ms.Opts.Flags.Args()) > 0 { + switch ms.Opts.Flags.Arg(0) { case "layout": return layoutHelp(ctx, ms) } @@ -62,25 +80,26 @@ func run(ctx context.Context, ms *xmain.State) (err error) { var inputPath string var outputPath string - if len(ms.FlagSet.Args()) == 0 { + if len(ms.Opts.Flags.Args()) == 0 { if versionFlag != nil && *versionFlag { fmt.Println(version.Version) return nil } help(ms) return nil - } else if len(ms.FlagSet.Args()) >= 3 { + } else if len(ms.Opts.Flags.Args()) >= 3 { return xmain.UsageErrorf("too many arguments passed") } - if len(ms.FlagSet.Args()) >= 1 { - if ms.FlagSet.Arg(0) == "version" { + + if len(ms.Opts.Flags.Args()) >= 1 { + if ms.Opts.Flags.Arg(0) == "version" { fmt.Println(version.Version) return nil } - inputPath = ms.FlagSet.Arg(0) + inputPath = ms.Opts.Flags.Arg(0) } - if len(ms.FlagSet.Args()) >= 2 { - outputPath = ms.FlagSet.Arg(1) + if len(ms.Opts.Flags.Args()) >= 2 { + outputPath = ms.Opts.Flags.Arg(1) } else { if inputPath == "-" { outputPath = "-" @@ -93,16 +112,11 @@ func run(ctx context.Context, ms *xmain.State) (err error) { if match == (d2themes.Theme{}) { return xmain.UsageErrorf("-t[heme] could not be found. The available options are:\n%s\nYou provided: %d", d2themescatalog.CLIString(), *themeFlag) } - ms.Env.Setenv("D2_THEME", fmt.Sprintf("%d", *themeFlag)) + ms.Log.Debug.Printf("using theme %s (ID: %d)", match.Name, *themeFlag) - envD2Layout := ms.Env.Getenv("D2_LAYOUT") - if envD2Layout == "" { - envD2Layout = "dagre" - } - - plugin, path, err := d2plugin.FindPlugin(ctx, envD2Layout) + plugin, path, err := d2plugin.FindPlugin(ctx, *layoutFlag) if errors.Is(err, exec.ErrNotFound) { - return layoutNotFound(ctx, envD2Layout) + return layoutNotFound(ctx, *layoutFlag) } else if err != nil { return err } @@ -111,7 +125,7 @@ func run(ctx context.Context, ms *xmain.State) (err error) { if path != "" { pluginLocation = fmt.Sprintf("executable plugin at %s", humanPath(path)) } - ms.Log.Debug.Printf("using layout plugin %s (%s)", envD2Layout, pluginLocation) + ms.Log.Debug.Printf("using layout plugin %s (%s)", *layoutFlag, pluginLocation) var pw png.Playwright if filepath.Ext(outputPath) == ".png" { @@ -133,7 +147,15 @@ func run(ctx context.Context, ms *xmain.State) (err error) { return xmain.UsageErrorf("-w[atch] cannot be combined with reading input from stdin") } ms.Env.Setenv("LOG_TIMESTAMPS", "1") - w, err := newWatcher(ctx, ms, plugin, inputPath, outputPath, pw) + w, err := newWatcher(ctx, ms, watcherOpts{ + layoutPlugin: plugin, + themeID: *themeFlag, + host: *hostFlag, + port: *portFlag, + inputPath: inputPath, + outputPath: outputPath, + pw: pw, + }) if err != nil { return err } @@ -147,7 +169,7 @@ func run(ctx context.Context, ms *xmain.State) (err error) { _ = 343 } - _, err = compile(ctx, ms, plugin, inputPath, outputPath, pw.Page) + _, err = compile(ctx, ms, plugin, *themeFlag, inputPath, outputPath, pw.Page) if err != nil { return err } @@ -156,7 +178,7 @@ func run(ctx context.Context, ms *xmain.State) (err error) { return nil } -func compile(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, inputPath, outputPath string, page playwright.Page) ([]byte, error) { +func compile(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, themeID int64, inputPath, outputPath string, page playwright.Page) ([]byte, error) { input, err := ms.ReadPath(inputPath) if err != nil { return nil, err @@ -167,7 +189,6 @@ func compile(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, input return nil, err } - themeID, _ := strconv.ParseInt(ms.Env.Getenv("D2_THEME"), 10, 64) d, err := d2.Compile(ctx, string(input), &d2.CompileOptions{ Layout: plugin.Layout, Ruler: ruler, @@ -197,7 +218,7 @@ func compile(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, input if err != nil { return nil, err } - return out, nil + return svg, nil } // newExt must include leading . diff --git a/cmd/d2/watch.go b/cmd/d2/watch.go index 669a24a6e..b49489df6 100644 --- a/cmd/d2/watch.go +++ b/cmd/d2/watch.go @@ -35,16 +35,24 @@ var devMode = false //go:embed static var staticFS embed.FS +type watcherOpts struct { + layoutPlugin d2plugin.Plugin + themeID int64 + host string + port string + inputPath string + outputPath string + pw png.Playwright +} + type watcher struct { ctx context.Context cancel context.CancelFunc wg sync.WaitGroup devMode bool - ms *xmain.State - layoutPlugin d2plugin.Plugin - inputPath string - outputPath string + ms *xmain.State + watcherOpts compileCh chan struct{} @@ -62,8 +70,6 @@ type watcher struct { resMu sync.Mutex res *compileResult - - pw png.Playwright } type compileResult struct { @@ -71,7 +77,7 @@ type compileResult struct { SVG string `json:"svg"` } -func newWatcher(ctx context.Context, ms *xmain.State, layoutPlugin d2plugin.Plugin, inputPath, outputPath string, pw png.Playwright) (*watcher, error) { +func newWatcher(ctx context.Context, ms *xmain.State, opts watcherOpts) (*watcher, error) { ctx, cancel := context.WithCancel(ctx) w := &watcher{ @@ -79,14 +85,11 @@ func newWatcher(ctx context.Context, ms *xmain.State, layoutPlugin d2plugin.Plug cancel: cancel, devMode: devMode, - ms: ms, - layoutPlugin: layoutPlugin, - inputPath: inputPath, - outputPath: outputPath, + ms: ms, + watcherOpts: opts, compileCh: make(chan struct{}, 1), wsclients: make(map[*wsclient]struct{}), - pw: pw, } err := w.init() if err != nil { @@ -342,7 +345,7 @@ func (w *watcher) compileLoop(ctx context.Context) error { w.pw = newPW } - b, err := compile(ctx, w.ms, w.layoutPlugin, w.inputPath, w.outputPath, w.pw.Page) + b, err := compile(ctx, w.ms, w.layoutPlugin, w.themeID, w.inputPath, w.outputPath, w.pw.Page) if err != nil { err = fmt.Errorf("failed to %scompile: %w", recompiledPrefix, err) w.ms.Log.Error.Print(err) @@ -368,18 +371,7 @@ func (w *watcher) compileLoop(ctx context.Context) error { } func (w *watcher) listen() error { - host := "localhost" - port := "0" - hostEnv := w.ms.Env.Getenv("HOST") - if hostEnv != "" { - host = hostEnv - } - portEnv := w.ms.Env.Getenv("PORT") - if portEnv != "" { - port = portEnv - } - - l, err := net.Listen("tcp", net.JoinHostPort(host, port)) + l, err := net.Listen("tcp", net.JoinHostPort(w.host, w.port)) if err != nil { return err } diff --git a/d2plugin/serve.go b/d2plugin/serve.go index 919eab48b..2db42d215 100644 --- a/d2plugin/serve.go +++ b/d2plugin/serve.go @@ -19,12 +19,12 @@ import ( // Also see execPlugin in exec.go for the d2 binary plugin protocol. func Serve(p Plugin) func(context.Context, *xmain.State) error { return func(ctx context.Context, ms *xmain.State) (err error) { - if len(ms.Args) < 1 { + if len(ms.Opts.Flags.Args()) < 1 { return errors.New("expected first argument to plugin binary to be function name") } - reqFunc := ms.Args[0] + reqFunc := ms.Opts.Flags.Arg(0) - switch ms.Args[0] { + switch ms.Opts.Flags.Arg(0) { case "info": return info(ctx, p, ms) case "layout": diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index 41c82f89a..bd8851e7b 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -65,8 +65,7 @@ language. Sometimes it gives controversial sentences -- don't use those. Script to generate one line of random text: ``` ipsum1() { - fortune | head -n1 | sed 's/^ *//;s/ *$//' | tr -d '\n' | pbcopy - echo "$(pbpaste -Prefer txt)" + fortune | head -n1 | sed 's/^ *//;s/ *$//' | tr -d '\n' | tee /dev/stderr | pbcopy } ``` diff --git a/docs/INSTALL.md b/docs/INSTALL.md new file mode 100644 index 000000000..cf6599adb --- /dev/null +++ b/docs/INSTALL.md @@ -0,0 +1,91 @@ +# install + +This file documents all the ways by which you can install D2. + + + +- [install.sh](#installsh) +- [macOS (Homebrew)](#macos-homebrew) +- [Standalone](#standalone) +- [From source](#from-source) + + + +## install.sh + +The recommended and easiest way to install is with our install script, which will detect +the OS and architecture you're on and use the best method: + +```sh +# With --dry-run 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 -- --dry-run +# If things look good, install for real. +curl -fsSL https://d2lang.com/install.sh | sh -s -- +``` + +For help on the terminal run, including the supported package managers and detection +methods: + +```sh +curl -fsSL https://d2lang.com/install.sh | sh -s -- --help +``` + +## macOS (Homebrew) + +If you're on macOS, you can alternatively install with `brew`. (the install script above +does this automatically if you have `brew` installed). + +```sh +brew tap terrastruct/d2 +brew install d2 +``` + +## Standalone + +We publish standalone release archives with every release on Github. +Download the `.tar.gz` release for your OS/ARCH combination and then run the following +inside the extracted directory to install: + +```sh +make install +``` + +Run the following to uninstall: + +```sh +make uninstall +``` + +You will be prompted for sudo/su/doas if root permissions are required for installation. +You can control the Unix hierarchy installation path with `PREFIX=`. For example: + +``` +# Install under ~/.local. +# Binaries will be at ~/.local/bin +# And manpages will be under ~/.local/share/man +# And supporting data like icons and fonts at ~/.local/share/d2 +make install PREFIX=$HOME/.local +``` + +The install script places the standalone release into `$PREFIX/lib/d2/d2-` +and we recommend doing the same with manually installed releases so that you +know where the release directory is for easy uninstall. + +> warn: Our binary releases aren't fully portable like normal Go binaries due to the C +> dependency on v8go for executing dagre. + +## From source + +Alternatively, you can always install from source: + +```sh +go install oss.terrastruct.com/d2/cmd/d2@latest +``` + +## Coming soon + +- Docker image +- Windows install +- rpm and deb packages +- homebrew core diff --git a/go.sum b/go.sum index 532eafc6f..1b89dac58 100644 --- a/go.sum +++ b/go.sum @@ -778,6 +778,8 @@ honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9 nhooyr.io/websocket v1.8.7 h1:usjR2uOr/zjjkVMy0lW+PPohFok7PCow5sDjLgX4P4g= nhooyr.io/websocket v1.8.7/go.mod h1:B70DZP8IakI65RVQ51MsWP/8jndNma26DVA/nFSCgW0= oss.terrastruct.com/cmdlog v0.0.0-20221116181457-07977d95ac37 h1:Xy1JKJHc4hcuwi57s0BvGUY16GjxTtBmLUybsuGDU7E= +oss.terrastruct.com/cmdlog v0.0.0-20221116181457-07977d95ac37 h1:Xy1JKJHc4hcuwi57s0BvGUY16GjxTtBmLUybsuGDU7E= +oss.terrastruct.com/cmdlog v0.0.0-20221116181457-07977d95ac37/go.mod h1:ROL3yxl2X+S3O+Rls00qdX6aMh+p1dF8IdxDRwDDpsg= oss.terrastruct.com/cmdlog v0.0.0-20221116181457-07977d95ac37/go.mod h1:ROL3yxl2X+S3O+Rls00qdX6aMh+p1dF8IdxDRwDDpsg= oss.terrastruct.com/diff v1.0.2-0.20221116222035-8bf4dd3ab541 h1:I9B1O1IJ6spivIQxbFRZmbhAwVeLwrcQRR1JbYUOvrI= oss.terrastruct.com/diff v1.0.2-0.20221116222035-8bf4dd3ab541/go.mod h1:ags2QDy/T6jr69hT6bpmAmhr2H98n9o8Atf3QlUJPiU= diff --git a/install.sh b/install.sh index 5cc51950f..678dabfe5 100755 --- a/install.sh +++ b/install.sh @@ -54,8 +54,30 @@ if [ -n "${DEBUG-}" ]; then fi tput() { - if [ -n "$TERM" ]; then - command tput "$@" + if should_color; then + TERM=${TERM:-xterm-256color} command tput "$@" + fi +} + +should_color() { + if [ -n "${COLOR-}" ]; then + if [ "$COLOR" = 0 -o "$COLOR" = false ]; then + _COLOR= + return 1 + elif [ "$COLOR" = 1 -o "$COLOR" = true ]; then + _COLOR=1 + return 0 + else + printf '$COLOR must be 0, 1, false or true but got %s' "$COLOR" >&2 + fi + fi + + if [ -t 1 ]; then + _COLOR=1 + return 0 + else + _COLOR= + return 1 fi } @@ -92,14 +114,14 @@ printfp() {( prefix="$1" shift - if [ -z "${COLOR:-}" ]; then - COLOR="$(get_rand_color "$prefix")" + if [ -z "${FGCOLOR-}" ]; then + FGCOLOR="$(get_rand_color "$prefix")" fi - printf '%s' "$(setaf "$COLOR" "$prefix")" - - if [ $# -gt 0 ]; then - printf ': ' - printf "$@" + should_color || true + if [ $# -eq 0 ]; then + printf '%s' "$(COLOR=${_COLOR-} setaf "$FGCOLOR" "$prefix")" + else + printf '%s: %s\n' "$(COLOR=${_COLOR-} setaf "$FGCOLOR" "$prefix")" "$(printf "$@")" fi )} @@ -107,7 +129,8 @@ catp() { prefix="$1" shift - sed "s/^/$(printfp "$prefix" '')/" + should_color || true + sed "s/^/$(COLOR=${_COLOR-} printfp "$prefix" '')/" } repeat() { @@ -121,51 +144,58 @@ strlen() { } echoerr() { - COLOR=1 echop err "$*" | humanpath>&2 + FGCOLOR=1 logp err "$*" | humanpath>&2 } caterr() { - COLOR=1 catp err "$@" | humanpath >&2 + FGCOLOR=1 logpcat err "$@" | humanpath >&2 } printferr() { - COLOR=1 printfp err "$@" | humanpath >&2 + FGCOLOR=1 logfp err "$@" | humanpath >&2 } logp() { - echop "$@" | humanpath >&2 + should_color >&2 || true + COLOR=${_COLOR-} echop "$@" | humanpath >&2 } logfp() { - printfp "$@" | humanpath >&2 + should_color >&2 || true + COLOR=${_COLOR-} printfp "$@" | humanpath >&2 } logpcat() { - catp "$@" | humanpath >&2 + should_color >&2 || true + COLOR=${_COLOR-} catp "$@" | humanpath >&2 } log() { - COLOR=5 logp log "$@" + FGCOLOR=5 logp log "$@" } logf() { - COLOR=5 logfp log "$@" + FGCOLOR=5 logfp log "$@" } logcat() { - COLOR=5 logpcat log "$@" + FGCOLOR=5 logpcat log "$@" } warn() { - COLOR=3 logp warn "$@" + FGCOLOR=3 logp warn "$@" } warnf() { - COLOR=3 logfp warn "$@" + FGCOLOR=3 logfp warn "$@" +} + +warncat() { + FGCOLOR=3 logpcat warn "$@" } sh_c() { - COLOR=3 logp exec "$*" + FGCOLOR=3 logp exec "$*" if [ -z "${DRY_RUN-}" ]; then eval "$@" fi @@ -194,6 +224,12 @@ header() { logp "/* $1 */" } +bigheader() { + logp "/** + * $1 + **/" +} + # humanpath replaces all occurrences of " $HOME" with " ~" # and all occurrences of '$HOME' with the literal '$HOME'. humanpath() { @@ -280,9 +316,8 @@ LIB_FLAG=1 # 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 -- +# flag_parse exits with a non zero code when there are no more flags +# to be parsed. 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. @@ -310,18 +345,15 @@ flag_parse() { # Remove everything before first equal sign. FLAGARG="${1#*=}" FLAGSHIFT=1 + return 0 ;; -) - FLAG= - FLAGRAW= - unset FLAGARG FLAGSHIFT=0 + return 1 ;; --) - FLAG= - FLAGRAW= - unset FLAGARG FLAGSHIFT=1 + return 1 ;; -*) # Remove leading hyphens. @@ -343,15 +375,13 @@ flag_parse() { ;; esac fi + return 0 ;; *) - FLAG= - FLAGRAW= - unset FLAGARG FLAGSHIFT=0 + return 1 ;; esac - return 0 } flag_reqarg() { @@ -452,8 +482,11 @@ usage: $arg0 [--dry-run] [--version vX.X.X] [--edge] [--method detect] [--prefix [--tala latest] [--force] [--uninstall] install.sh automates the installation of D2 onto your system. It currently only supports -the installation of standalone releases from GitHub. If you pass --edge, it will clone the -source, build a release and install from it. +the installation of standalone releases from GitHub and via Homebrew on macOS. See the +docs for --detect below for more information + +If you pass --edge, it will clone the source, build a release and install from it. +--edge is incompatible with --tala and currently unimplemented. Flags: @@ -463,6 +496,8 @@ Flags: --version vX.X.X Pass to have install.sh install the given version instead of the latest version. + warn: The version may not be obeyed with package manager installations. Use + --method=standalone to enforce the version. --edge Pass to build and install D2 from source. This will still use --method if set to detect @@ -470,14 +505,15 @@ Flags: if an unsupported package manager is used. To install from source like a dev would, use go install oss.terrastruct.com/d2 note: currently unimplemented. + warn: incompatible with --tala as TALA is closed source. ---method [detect | standalone] +--method [detect | standalone | homebrew ] Pass to control the method by which to install. Right now we only support standalone releases from GitHub but later we'll add support for brew, rpm, deb and more. - note: currently unimplemented. - - detect is currently unimplemented but would use your OS's package manager - automatically. + - detect will use your OS's package manager automatically. + So far it only detects macOS and automatically uses homebrew. + - homebrew uses https://brew.sh/ which is a macOS and Linux package manager. - standalone installs a standalone release archive into the unix hierarchy path specified by --prefix which defaults to /usr/local Ensure /usr/local/bin is in your \$PATH to use it. @@ -485,16 +521,19 @@ Flags: --prefix /usr/local Controls the unix hierarchy path into which standalone releases are installed. Defaults to /usr/local. You may also want to use ~/.local to avoid needing sudo. - Remember that whatever you use, you must have the bin directory of your prefix - path in \$PATH to execute the d2 binary. For example, if my prefix directory is + We use ~/.local by default on arm64 macOS machines as SIP now disables access to + /usr/local. Remember that whatever you use, you must have the bin directory of your + prefix path in \$PATH to execute the d2 binary. For example, if my prefix directory is /usr/local then my \$PATH must contain /usr/local/bin. --tala [latest] Install Terrastruct's closed source TALA for improved layouts. - See https://github.com/terrastruct/TALA + See https://github.com/terrastruct/tala It optionally takes an argument of the TALA version to install. Installation obeys all other flags, just like the installation of d2. For example, the d2plugin-tala binary will be installed into /usr/local/bin/d2plugin-tala + warn: The version may not be obeyed with package manager installations. Use + --method=standalone to enforce the version. --force: Force installation over the existing version even if they match. It will attempt a @@ -507,6 +546,7 @@ Flags: 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. + note: tala will also be uninstalled if installed. 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- @@ -519,9 +559,7 @@ EOF } main() { - METHOD=standalone - while :; do - flag_parse "$@" + while flag_parse "$@"; do case "$FLAG" in h|help) help @@ -548,8 +586,6 @@ main() { method) flag_nonemptyarg && shift "$FLAGSHIFT" METHOD=$FLAGARG - echoerr "$FLAGRAW is currently unimplemented" - return 1 ;; prefix) flag_nonemptyarg && shift "$FLAGSHIFT" @@ -563,15 +599,12 @@ main() { flag_noarg && shift "$FLAGSHIFT" UNINSTALL=1 ;; - '') - shift "$FLAGSHIFT" - break - ;; *) flag_errusage "unrecognized flag $FLAGRAW" ;; esac done + shift "$FLAGSHIFT" if [ $# -gt 0 ]; then flag_errusage "no arguments are accepted" @@ -587,49 +620,86 @@ main() { PREFIX=${PREFIX:-/usr/local} CACHE_DIR=$(cache_dir) mkdir -p "$CACHE_DIR" + METHOD=${METHOD:-detect} INSTALL_DIR=$PREFIX/lib/d2 + case $METHOD in + detect) + case "$OS" in + macos) + if command -v brew >/dev/null; then + log "detected macOS with homebrew, using homebrew for (un)installation" + METHOD=homebrew + else + warn "detected macOS without homebrew, falling back to --method=standalone" + METHOD=standalone + fi + ;; + *) + warn "unrecognized OS $OS, falling back to --method=standalone" + METHOD=standalone + ;; + esac + ;; + standalone) ;; + homebrew) ;; + *) + echoerr "unknown (un)installation method $METHOD" + return 1 + ;; + esac + if [ -n "${UNINSTALL-}" ]; then uninstall - return 0 + if [ -n "${DRY_RUN-}" ]; then + log "Rerun without --dry-run to execute printed commands and perform uninstall." + fi + else + install + if [ -n "${DRY_RUN-}" ]; then + log "Rerun without --dry-run to execute printed commands and perform install." + fi fi - - VERSION=${VERSION:-latest} - if [ "$VERSION" = latest ]; then - header "fetching latest release info" - fetch_release_info - fi - - install } install() { - install_d2 - if [ -n "${TALA-}" ]; then - # Run in subshell to avoid overwriting VERSION. - TALA_VERSION="$( install_tala && echo "$VERSION" )" - fi + case $METHOD in + standalone) + install_d2_standalone + if [ -n "${TALA-}" ]; then + # Run in subshell to avoid overwriting VERSION. + TALA_VERSION="$( RELEASE_INFO= install_tala_standalone && echo "$VERSION" )" + fi + ;; + homebrew) + install_d2_brew + if [ -n "${TALA-}" ]; then install_tala_brew; fi + ;; + esac - COLOR=2 header success + FGCOLOR=2 bigheader 'next steps' + case $METHOD in + standalone) install_post_standalone ;; + homebrew) install_post_brew ;; + esac +} + +install_post_standalone() { log "d2-$VERSION-$OS-$ARCH has been successfully installed into $PREFIX" if [ -n "${TALA-}" ]; then log "tala-$TALA_VERSION-$OS-$ARCH has been successfully installed into $PREFIX" fi - log "Rerun this install script with --uninstall to uninstall" + log "Rerun this install script with --uninstall to uninstall." + log if ! echo "$PATH" | grep -qF "$PREFIX/bin"; then logcat >&2 <&2 <&2 <&2 </dev/null; then INSTALLED_VERSION="$(d2 version)" if [ ! "${FORCE-}" -a "$VERSION" = "$INSTALLED_VERSION" ]; then - log "skipping installation as version $VERSION is already installed." + log "skipping installation as d2 $VERSION is already installed." return 0 fi - log "uninstalling $INSTALLED_VERSION to install $VERSION" - if ! uninstall_d2; then - warn "failed to uninstall $INSTALLED_VERSION" + log "uninstalling d2 $INSTALLED_VERSION to install $VERSION" + if ! uninstall_d2_standalone; then + warn "failed to uninstall d2 $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" @@ -690,19 +785,38 @@ install_standalone_d2() { "$sh_c" sh -c "'cd \"$INSTALL_DIR/d2-$VERSION\" && make install PREFIX=\"$PREFIX\"'" } -install_tala() { - REPO="${REPO_TALA:-terrastruct/TALA}" - VERSION=$TALA - RELEASE_INFO= - fetch_release_info - header "installing tala-$VERSION" - install_standalone_tala +install_d2_brew() { + header "installing d2 with homebrew" + sh_c brew tap terrastruct/d2 + sh_c brew install d2 } -install_standalone_tala() { +install_tala_standalone() { + REPO="${REPO_TALA:-terrastruct/tala}" + VERSION=$TALA + + header "installing tala-$VERSION" + + if [ "$VERSION" = latest ]; then + fetch_release_info + fi + + if command -v d2plugin-tala >/dev/null; then + INSTALLED_VERSION="$(d2plugin-tala --version)" + if [ ! "${FORCE-}" -a "$VERSION" = "$INSTALLED_VERSION" ]; then + log "skipping installation as tala $VERSION is already installed." + return 0 + fi + log "uninstalling tala $INSTALLED_VERSION to install $VERSION" + if ! uninstall_tala_standalone; then + warn "failed to uninstall tala $INSTALLED_VERSION" + fi + fi + ARCHIVE="tala-$VERSION-$OS-$ARCH.tar.gz" log "installing standalone release $ARCHIVE from github" + fetch_release_info asset_line=$(sh_c 'cat "$RELEASE_INFO" | grep -n "$ARCHIVE" | cut -d: -f1 | head -n1') asset_url=$(sh_c 'sed -n $((asset_line-3))p "$RELEASE_INFO" | sed "s/^.*: \"\(.*\)\",$/\1/g"') @@ -718,36 +832,40 @@ install_standalone_tala() { "$sh_c" sh -c "'cd \"$INSTALL_DIR/tala-$VERSION\" && make install PREFIX=\"$PREFIX\"'" } +install_tala_brew() { + header "installing tala with homebrew" + sh_c brew tap terrastruct/d2 + sh_c brew install tala +} + uninstall() { + # We uninstall tala first as package managers require that it be uninstalled before + # uninstalling d2 as TALA depends on d2. + if command -v d2plugin-tala >/dev/null; then + INSTALLED_VERSION="$(d2plugin-tala --version)" + header "uninstalling tala-$INSTALLED_VERSION" + case $METHOD in + standalone) uninstall_tala_standalone ;; + homebrew) uninstall_tala_brew ;; + esac + elif [ "${TALA-}" ]; then + warn "no version of tala installed" + fi + if ! command -v d2 >/dev/null; then warn "no version of d2 installed" return 0 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 - warn "no version of tala installed" - return 0 - 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 + case $METHOD in + standalone) uninstall_d2_standalone ;; + homebrew) uninstall_d2_brew ;; + esac } -uninstall_standalone_d2() { +uninstall_d2_standalone() { log "uninstalling standalone release of d2-$INSTALLED_VERSION" if [ ! -e "$INSTALL_DIR/d2-$INSTALLED_VERSION" ]; then @@ -765,12 +883,11 @@ uninstall_standalone_d2() { "$sh_c" rm -rf "$INSTALL_DIR/d2-$INSTALLED_VERSION" } -uninstall_tala() { - header "uninstalling tala-$INSTALLED_VERSION" - uninstall_standalone_tala +uninstall_d2_brew() { + sh_c brew remove d2 } -uninstall_standalone_tala() { +uninstall_tala_standalone() { log "uninstalling standalone release tala-$INSTALLED_VERSION" if [ ! -e "$INSTALL_DIR/tala-$INSTALLED_VERSION" ]; then @@ -788,6 +905,10 @@ uninstall_standalone_tala() { "$sh_c" rm -rf "$INSTALL_DIR/tala-$INSTALLED_VERSION" } +uninstall_tala_brew() { + sh_c brew remove tala +} + is_prefix_writable() { sh_c "mkdir -p '$INSTALL_DIR' 2>/dev/null" || true # The reason for checking whether $INSTALL_DIR is writable is that on macOS you have @@ -843,4 +964,9 @@ fetch_gh() { sh_c mv "$file.inprogress" "$file" } +brew() { + # Makes brew sane. + HOMEBREW_NO_INSTALL_CLEANUP=1 HOMEBREW_NO_AUTO_UPDATE=1 command brew "$@" +} + main "$@" diff --git a/lib/version/version.go b/lib/version/version.go index 34ab1a5bf..2f1ff62e5 100644 --- a/lib/version/version.go +++ b/lib/version/version.go @@ -1,4 +1,4 @@ package version // Pre-built binaries will have version set during build time. -var Version = "master (built from source)" +var Version = "????" diff --git a/lib/xmain/flag_helpers.go b/lib/xmain/flag_helpers.go new file mode 100644 index 000000000..4d17066dc --- /dev/null +++ b/lib/xmain/flag_helpers.go @@ -0,0 +1,45 @@ +// flag_helpers.go are private functions from pflag/flag.go +package xmain + +import "strings" + +func wrap(i, w int, s string) string { + if w == 0 { + return strings.Replace(s, "\n", "\n"+strings.Repeat(" ", i), -1) + } + wrap := w - i + var r, l string + if wrap < 24 { + i = 16 + wrap = w - i + r += "\n" + strings.Repeat(" ", i) + } + if wrap < 24 { + return strings.Replace(s, "\n", r, -1) + } + slop := 5 + wrap = wrap - slop + l, s = wrapN(wrap, slop, s) + r = r + strings.Replace(l, "\n", "\n"+strings.Repeat(" ", i), -1) + for s != "" { + var t string + t, s = wrapN(wrap, slop, s) + r = r + "\n" + strings.Repeat(" ", i) + strings.Replace(t, "\n", "\n"+strings.Repeat(" ", i), -1) + } + return r +} + +func wrapN(i, slop int, s string) (string, string) { + if i+slop > len(s) { + return s, "" + } + w := strings.LastIndexAny(s[:i], " \t\n") + if w <= 0 { + return s, "" + } + nlPos := strings.LastIndex(s[:i], "\n") + if nlPos > 0 && nlPos < w { + return s[:nlPos], s[nlPos+1:] + } + return s[:w], s[w+1:] +} diff --git a/lib/xmain/opts.go b/lib/xmain/opts.go new file mode 100644 index 000000000..98d9387ef --- /dev/null +++ b/lib/xmain/opts.go @@ -0,0 +1,173 @@ +package xmain + +import ( + "bytes" + "fmt" + "io" + "strconv" + "strings" + + "github.com/spf13/pflag" + "oss.terrastruct.com/cmdlog" + "oss.terrastruct.com/xos" +) + +type Opts struct { + Args []string + Flags *pflag.FlagSet + env *xos.Env + log *cmdlog.Logger + + flagEnv map[string]string +} + +func NewOpts(env *xos.Env, log *cmdlog.Logger, args []string) *Opts { + flags := pflag.NewFlagSet("", pflag.ContinueOnError) + flags.SortFlags = false + flags.Usage = func() {} + flags.SetOutput(io.Discard) + return &Opts{ + Args: args, + Flags: flags, + env: env, + log: log, + flagEnv: make(map[string]string), + } +} + +// Mostly copy pasted pasted from pflag.FlagUsagesWrapped +// with modifications for env var +func (o *Opts) Defaults() string { + buf := new(bytes.Buffer) + + var lines []string + + maxlen := 0 + maxEnvLen := 0 + o.Flags.VisitAll(func(flag *pflag.Flag) { + if flag.Hidden { + return + } + + line := "" + if flag.Shorthand != "" && flag.ShorthandDeprecated == "" { + line = fmt.Sprintf(" -%s, --%s", flag.Shorthand, flag.Name) + } else { + line = fmt.Sprintf(" --%s", flag.Name) + } + + varname, usage := pflag.UnquoteUsage(flag) + if varname != "" { + line += " " + varname + } + if flag.NoOptDefVal != "" { + switch flag.Value.Type() { + case "string": + line += fmt.Sprintf("[=\"%s\"]", flag.NoOptDefVal) + case "bool": + if flag.NoOptDefVal != "true" { + line += fmt.Sprintf("[=%s]", flag.NoOptDefVal) + } + case "count": + if flag.NoOptDefVal != "+1" { + line += fmt.Sprintf("[=%s]", flag.NoOptDefVal) + } + default: + line += fmt.Sprintf("[=%s]", flag.NoOptDefVal) + } + } + + line += "\x00" + + if len(line) > maxlen { + maxlen = len(line) + } + + if e, ok := o.flagEnv[flag.Name]; ok { + line += fmt.Sprintf("$%s", e) + } + + line += "\x01" + + if len(line) > maxEnvLen { + maxEnvLen = len(line) + } + + line += usage + if flag.Value.Type() == "string" { + line += fmt.Sprintf(" (default %q)", flag.DefValue) + } else { + line += fmt.Sprintf(" (default %s)", flag.DefValue) + } + if len(flag.Deprecated) != 0 { + line += fmt.Sprintf(" (DEPRECATED: %s)", flag.Deprecated) + } + + lines = append(lines, line) + }) + + for _, line := range lines { + sidx1 := strings.Index(line, "\x00") + sidx2 := strings.Index(line, "\x01") + spacing1 := strings.Repeat(" ", maxlen-sidx1) + spacing2 := strings.Repeat(" ", (maxEnvLen-maxlen)-sidx2+sidx1) + fmt.Fprintln(buf, line[:sidx1], spacing1, line[sidx1+1:sidx2], spacing2, wrap(maxEnvLen+3, 0, line[sidx2+1:])) + } + + return buf.String() +} + +func (o *Opts) getEnv(flag, k string) string { + if k != "" { + o.flagEnv[flag] = k + return o.env.Getenv(k) + } + return "" +} + +func (o *Opts) Int64(envKey, flag, shortFlag string, defaultVal int64, usage string) (*int64, error) { + if env := o.getEnv(flag, envKey); env != "" { + envVal, err := strconv.ParseInt(env, 10, 64) + if err != nil { + return nil, fmt.Errorf(`invalid environment variable %s. Expected int64. Found "%v".`, envKey, envVal) + } + defaultVal = envVal + } + + return o.Flags.Int64P(flag, shortFlag, defaultVal, usage), nil +} + +func (o *Opts) String(envKey, flag, shortFlag string, defaultVal, usage string) *string { + if env := o.getEnv(flag, envKey); env != "" { + defaultVal = env + } + + return o.Flags.StringP(flag, shortFlag, defaultVal, usage) +} + +func (o *Opts) Bool(envKey, flag, shortFlag string, defaultVal bool, usage string) (*bool, error) { + if env := o.getEnv(flag, envKey); env != "" { + if !boolyEnv(env) { + return nil, fmt.Errorf(`invalid environment variable %s. Expected bool. Found "%s".`, envKey, env) + } + if truthyEnv(env) { + defaultVal = true + } else { + defaultVal = false + } + } + + return o.Flags.BoolP(flag, shortFlag, defaultVal, usage), nil +} + +func boolyEnv(s string) bool { + return falseyEnv(s) || truthyEnv(s) +} + +func falseyEnv(s string) bool { + return s == "0" || s == "false" +} + +func truthyEnv(s string) bool { + return s == "1" || s == "true" +} diff --git a/lib/xmain/xmain.go b/lib/xmain/xmain.go index d71147776..18eba1367 100644 --- a/lib/xmain/xmain.go +++ b/lib/xmain/xmain.go @@ -9,13 +9,11 @@ import ( "io" "os" "os/signal" - "strings" "syscall" "time" "cdr.dev/slog" "cdr.dev/slog/sloggers/sloghuman" - "github.com/spf13/pflag" "oss.terrastruct.com/xos" @@ -41,14 +39,10 @@ func Main(run RunFunc) { Stdout: os.Stdout, Stderr: os.Stderr, - Env: xos.NewEnv(os.Environ()), - FlagSet: pflag.NewFlagSet("", pflag.ContinueOnError), - Args: args, + Env: xos.NewEnv(os.Environ()), } ms.Log = cmdlog.Log(ms.Env, os.Stderr) - ms.FlagSet.SortFlags = false - ms.FlagSet.Usage = func() {} - ms.FlagSet.SetOutput(io.Discard) + ms.Opts = NewOpts(ms.Env, ms.Log, args) sigs := make(chan os.Signal, 1) signal.Notify(sigs, os.Interrupt, syscall.SIGTERM) @@ -88,10 +82,9 @@ type State struct { Stdout io.WriteCloser Stderr io.WriteCloser - Log *cmdlog.Logger - Env *xos.Env - Args []string - FlagSet *pflag.FlagSet + Log *cmdlog.Logger + Env *xos.Env + Opts *Opts } func (ms *State) Main(ctx context.Context, sigs <-chan os.Signal, run func(context.Context, *State) error) error { @@ -129,13 +122,6 @@ func (ms *State) Main(ctx context.Context, sigs <-chan os.Signal, run func(conte } } -func (ms *State) FlagHelp() string { - b := &strings.Builder{} - ms.FlagSet.SetOutput(b) - ms.FlagSet.PrintDefaults() - return b.String() -} - type ExitError struct { Code int `json:"code"` Message string `json:"message"`