Merge branch 'master' into bernie/export-png

This commit is contained in:
Bernard Xie 2022-11-21 11:00:26 -08:00
commit e8340ff610
No known key found for this signature in database
GPG key ID: 3C3E0036CE0F892C
28 changed files with 1099 additions and 394 deletions

View file

@ -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 }}

View file

@ -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 }}

View file

@ -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 ./...

View file

@ -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

11
ci/gen.sh Executable file
View file

@ -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

View file

@ -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-<VERSION>
@ -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
VERSION=${VERSION:-latest}
if [ "$VERSION" = latest ]; then
header "fetching latest release info"
fetch_release_info
fi
else
install
if [ -n "${DRY_RUN-}" ]; then
log "Rerun without --dry-run to execute printed commands and perform install."
fi
fi
}
install() {
install_d2
case $METHOD in
standalone)
install_d2_standalone
if [ -n "${TALA-}" ]; then
# Run in subshell to avoid overwriting VERSION.
TALA_VERSION="$( install_tala && echo "$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 <<EOF
%%%%%%%%%%%%%%%%%%%%%%%%%
NEXT STEPS
%%%%%%%%%%%%%%%%%%%%%%%%%
Extend your \$PATH to use d2:
export PATH=$PREFIX/bin:\$PATH
Then run:
${TALA+D2_LAYOUT=tala }d2 --help
EOF
else
log " Run ${TALA+D2_LAYOUT=tala }d2 --help for usage."
log "Run ${TALA+D2_LAYOUT=tala }d2 --help for usage."
fi
if ! manpath | grep -qF "$PREFIX/share/man"; then
logcat >&2 <<EOF
@ -208,36 +248,61 @@ EOF
log " man d2plugin-tala"
fi
else
log " Run man d2 for detailed docs."
log "Run man d2 for detailed docs."
if [ -n "${TALA-}" ]; then
log " Run man d2plugin-tala for detailed docs."
log "Run man d2plugin-tala for detailed TALA docs."
fi
fi
logcat >&2 <<EOF
Something not working? Please let us know:
https://github.com/terrastruct/d2/issues/new
https://github.com/terrastruct/d2/issues
https://github.com/terrastruct/d2/discussions
https://discord.gg/NF6X8K4eDq
EOF
}
install_d2() {
install_post_brew() {
log "d2 has been successfully installed with homebrew."
if [ -n "${TALA-}" ]; then
log "tala has been successfully installed with homebrew."
fi
log "Rerun this install script with --uninstall to uninstall."
log
log "Run ${TALA+D2_LAYOUT=tala }d2 --help for usage."
log "Run man d2 for detailed docs."
if [ -n "${TALA-}" ]; then
log "Run man d2plugin-tala for detailed TALA docs."
fi
logcat >&2 <<EOF
Something not working? Please let us know:
https://github.com/terrastruct/d2/issues
https://github.com/terrastruct/d2/discussions
https://discord.gg/NF6X8K4eDq
EOF
}
install_d2_standalone() {
VERSION=${VERSION:-latest}
header "installing d2-$VERSION"
if [ "$VERSION" = latest ]; then
fetch_release_info
fi
if command -v d2 >/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 "$@"

View file

@ -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"

View file

@ -13,5 +13,4 @@ docker_run \
-e OS \
-e ARCH \
-e ARCHIVE \
-e TERM \
"$tag" ./src/d2/ci/release/_build.sh

View file

@ -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

View file

@ -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"

View file

@ -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" "$@"

View file

@ -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 🔴

29
ci/release/gen_template_lib.sh Executable file
View file

@ -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 <<EOF
#!/bin/sh
# *************
# 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.
# *************
EOF
# sed removes the sourcing dependency lines as we're bundled everything into a single
# script.
sh_c cat \
./ci/sub/lib/rand.sh \
./ci/sub/lib/log.sh \
\| sed "-e'/^\. /d'" \>\>./ci/release/template/scripts/lib.sh
sh_c chmod -w ./ci/release/template/scripts/lib.sh

View file

@ -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

View file

@ -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 <<EOF
This script needs to run the following command as root:
$*
Please install doas, sudo, or su.
EOF
return 1
fi
}
header() {
logp "/* $1 */"
}
bigheader() {
logp "/**
* $1
**/"
}
# humanpath replaces all occurrences of " $HOME" with " ~"
# and all occurrences of '$HOME' with the literal '$HOME'.
humanpath() {
if [ -z "${HOME-}" ]; then
cat
else
sed -e "s# $HOME# ~#g" -e "s#$HOME#\$HOME#g"
fi
}
hide() {
out="$(mktemp)"
set +e
"$@" >"$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
}

2
ci/sub

@ -1 +1 @@
Subproject commit df51b90892737ebe9feca3dd982bcdfc7f684834
Subproject commit 824046d952b1442c76a057553591652c889fb7cb

View file

@ -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)
}

View file

@ -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 .

View file

@ -35,6 +35,16 @@ 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
@ -42,9 +52,7 @@ type watcher struct {
devMode bool
ms *xmain.State
layoutPlugin d2plugin.Plugin
inputPath string
outputPath string
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{
@ -80,13 +86,10 @@ func newWatcher(ctx context.Context, ms *xmain.State, layoutPlugin d2plugin.Plug
devMode: devMode,
ms: ms,
layoutPlugin: layoutPlugin,
inputPath: inputPath,
outputPath: outputPath,
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
}

View file

@ -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":

View file

@ -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
}
```

91
docs/INSTALL.md Normal file
View file

@ -0,0 +1,91 @@
# install
This file documents all the ways by which you can install D2.
<!-- toc -->
- [install.sh](#installsh)
- [macOS (Homebrew)](#macos-homebrew)
- [Standalone](#standalone)
- [From source](#from-source)
<!-- tocstop -->
## 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-<version>`
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

2
go.sum
View file

@ -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=

View file

@ -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-<VERSION>
@ -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
VERSION=${VERSION:-latest}
if [ "$VERSION" = latest ]; then
header "fetching latest release info"
fetch_release_info
fi
else
install
if [ -n "${DRY_RUN-}" ]; then
log "Rerun without --dry-run to execute printed commands and perform install."
fi
fi
}
install() {
install_d2
case $METHOD in
standalone)
install_d2_standalone
if [ -n "${TALA-}" ]; then
# Run in subshell to avoid overwriting VERSION.
TALA_VERSION="$( install_tala && echo "$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 <<EOF
%%%%%%%%%%%%%%%%%%%%%%%%%
NEXT STEPS
%%%%%%%%%%%%%%%%%%%%%%%%%
Extend your \$PATH to use d2:
export PATH=$PREFIX/bin:\$PATH
Then run:
${TALA+D2_LAYOUT=tala }d2 --help
EOF
else
log " Run ${TALA+D2_LAYOUT=tala }d2 --help for usage."
log "Run ${TALA+D2_LAYOUT=tala }d2 --help for usage."
fi
if ! manpath | grep -qF "$PREFIX/share/man"; then
logcat >&2 <<EOF
@ -642,36 +712,61 @@ EOF
log " man d2plugin-tala"
fi
else
log " Run man d2 for detailed docs."
log "Run man d2 for detailed docs."
if [ -n "${TALA-}" ]; then
log " Run man d2plugin-tala for detailed docs."
log "Run man d2plugin-tala for detailed TALA docs."
fi
fi
logcat >&2 <<EOF
Something not working? Please let us know:
https://github.com/terrastruct/d2/issues/new
https://github.com/terrastruct/d2/issues
https://github.com/terrastruct/d2/discussions
https://discord.gg/NF6X8K4eDq
EOF
}
install_d2() {
install_post_brew() {
log "d2 has been successfully installed with homebrew."
if [ -n "${TALA-}" ]; then
log "tala has been successfully installed with homebrew."
fi
log "Rerun this install script with --uninstall to uninstall."
log
log "Run ${TALA+D2_LAYOUT=tala }d2 --help for usage."
log "Run man d2 for detailed docs."
if [ -n "${TALA-}" ]; then
log "Run man d2plugin-tala for detailed TALA docs."
fi
logcat >&2 <<EOF
Something not working? Please let us know:
https://github.com/terrastruct/d2/issues
https://github.com/terrastruct/d2/discussions
https://discord.gg/NF6X8K4eDq
EOF
}
install_d2_standalone() {
VERSION=${VERSION:-latest}
header "installing d2-$VERSION"
if [ "$VERSION" = latest ]; then
fetch_release_info
fi
if command -v d2 >/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 "$@"

View file

@ -1,4 +1,4 @@
package version
// Pre-built binaries will have version set during build time.
var Version = "master (built from source)"
var Version = "????"

45
lib/xmain/flag_helpers.go Normal file
View file

@ -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:]
}

173
lib/xmain/opts.go Normal file
View file

@ -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"
}

View file

@ -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"
@ -42,13 +40,9 @@ func Main(run RunFunc) {
Stderr: os.Stderr,
Env: xos.NewEnv(os.Environ()),
FlagSet: pflag.NewFlagSet("", pflag.ContinueOnError),
Args: args,
}
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)
@ -90,8 +84,7 @@ type State struct {
Log *cmdlog.Logger
Env *xos.Env
Args []string
FlagSet *pflag.FlagSet
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"`