diff --git a/ci/release/Dockerfile b/ci/release/Dockerfile new file mode 100644 index 000000000..d195fa05b --- /dev/null +++ b/ci/release/Dockerfile @@ -0,0 +1,18 @@ +# https://hub.docker.com/repository/docker/terrastruct/d2 +FROM debian:latest + +ARG TARGETARCH + +COPY ./d2-*-linux-$TARGETARCH.tar.gz /tmp +RUN mkdir -p /usr/local/lib/d2 \ + && tar -C /usr/local/lib/d2 -xzf /tmp/d2-*-linux-"$TARGETARCH".tar.gz \ + && /usr/local/lib/d2/d2-*/scripts/install.sh \ + && rm -Rf /tmp/d2-*-linux-"$TARGETARCH".tar.gz + +WORKDIR /root/src +EXPOSE 8080 +ENV PORT 8080 +ENV HOST 0.0.0.0 +ENV BROWSER false + +ENTRYPOINT ["/usr/local/bin/d2"] diff --git a/ci/release/README.md b/ci/release/README.md index 4530983e4..4f8b1a56d 100644 --- a/ci/release/README.md +++ b/ci/release/README.md @@ -2,7 +2,7 @@ ## _install.sh -The template for the install script in the root of the repository. +The template for the install script in the root of the d2 repository. ### gen_install.sh @@ -23,12 +23,12 @@ it depends on from ../sub/lib. > variables as we must compile d2 directly on each release target to include dagre. > See https://github.com/terrastruct/d2/issues/31 -Use `--host-only` to build only the release for the host's OS-ARCH pair. +Use `--host-only` to build only the release for the host's `$OS-$ARCH` pair. ### build_docker.sh Helper script called by build.sh to build D2 on each linux runner inside Docker. -The Dockerfile is in ./builders/Dockerfile +The Dockerfile is in ./linux/Dockerfile ### _build.sh diff --git a/ci/release/aws/ensure.sh b/ci/release/aws/ensure.sh new file mode 100755 index 000000000..5f68e45b3 --- /dev/null +++ b/ci/release/aws/ensure.sh @@ -0,0 +1,545 @@ +#!/bin/sh +set -eu +. "$(dirname "$0")/../../../ci/sub/lib.sh" +cd -- "$(dirname "$0")/../../.." + +help() { + cat </dev/null \ + | jq -r .SecurityGroups[0].GroupId) + if [ -z "$SG_ID" ]; then + SG_ID=$(sh_c aws ec2 create-security-group \ + --group-name ssh \ + --description ssh \ + --vpc-id "$VPC_ID" | jq -r .GroupId) + fi + + header security-group-ingress + SG_RULES_COUNT=$(aws ec2 describe-security-groups --group-names ssh \ + | jq -r '.SecurityGroups[0].IpPermissions | length') + if [ "$SG_RULES_COUNT" -eq 0 ]; then + sh_c aws ec2 authorize-security-group-ingress \ + --group-id "$SG_ID" \ + --protocol tcp \ + --port 22 \ + --cidr 0.0.0.0/0 >/dev/null + fi + + header windows-security-group + SG_ID=$(aws ec2 describe-security-groups --group-names windows 2>/dev/null \ + | jq -r .SecurityGroups[0].GroupId) + if [ -z "$SG_ID" ]; then + SG_ID=$(sh_c aws ec2 create-security-group \ + --group-name windows \ + --description windows \ + --vpc-id "$VPC_ID" | jq -r .GroupId) + fi + + header windows-security-group-ingress + SG_RULES_COUNT=$(aws ec2 describe-security-groups --group-names windows \ + | jq -r '.SecurityGroups[0].IpPermissions | length') + if [ "$SG_RULES_COUNT" -ne 2 ]; then + sh_c aws ec2 authorize-security-group-ingress \ + --group-id "$SG_ID" \ + --protocol tcp \ + --port 22 \ + --cidr 0.0.0.0/0 >/dev/null + sh_c aws ec2 authorize-security-group-ingress \ + --group-id "$SG_ID" \ + --protocol tcp \ + --port 3389 \ + --cidr 0.0.0.0/0 >/dev/null + fi +} + +create_linux_amd64() { + header linux-amd64 + REMOTE_NAME=ci-d2-linux-amd64 + state=$(aws ec2 describe-instances --filters \ + 'Name=instance-state-name,Values=pending,running,stopping,stopped' 'Name=tag:Name,Values=ci-d2-linux-amd64' \ + | jq -r '.Reservations[].Instances[].State.Name') + if [ -z "$state" ]; then + sh_c aws ec2 run-instances \ + --image-id=ami-0ecc74eca1d66d8a6 \ + --count=1 \ + --instance-type=t3.small \ + --security-groups=ssh \ + "--key-name=$KEY_NAME" \ + --iam-instance-profile 'Name=AmazonSSMRoleForInstancesQuickSetup' \ + --block-device-mappings '"DeviceName=/dev/sda1,Ebs={VolumeSize=64,VolumeType=gp3}"' \ + --tag-specifications '"ResourceType=instance,Tags=[{Key=Name,Value=ci-d2-linux-amd64}]"' \ + '"ResourceType=volume,Tags=[{Key=Name,Value=ci-d2-linux-amd64}]"' >/dev/null + fi + wait_remote_host_ip + log "CI_D2_LINUX_AMD64=ubuntu@$ip" + export CI_D2_LINUX_AMD64=ubuntu@$ip +} + +create_linux_arm64() { + header linux-arm64 + REMOTE_NAME=ci-d2-linux-arm64 + state=$(aws ec2 describe-instances --filters \ + 'Name=instance-state-name,Values=pending,running,stopping,stopped' 'Name=tag:Name,Values=ci-d2-linux-arm64' \ + | jq -r '.Reservations[].Instances[].State.Name') + if [ -z "$state" ]; then + sh_c aws ec2 run-instances \ + --image-id=ami-06e2dea2cdda3acda \ + --count=1 \ + --instance-type=t4g.small \ + --security-groups=ssh \ + "--key-name=$KEY_NAME" \ + --iam-instance-profile 'Name=AmazonSSMRoleForInstancesQuickSetup' \ + --block-device-mappings '"DeviceName=/dev/sda1,Ebs={VolumeSize=64,VolumeType=gp3}"' \ + --tag-specifications '"ResourceType=instance,Tags=[{Key=Name,Value=ci-d2-linux-arm64}]"' \ + '"ResourceType=volume,Tags=[{Key=Name,Value=ci-d2-linux-arm64}]"' >/dev/null + fi + wait_remote_host_ip + log "CI_D2_LINUX_ARM64=ubuntu@$ip" + export CI_D2_LINUX_ARM64=ubuntu@$ip +} + +create_macos_amd64() { + header macos-amd64-host + MACOS_AMD64_ID=$(aws ec2 describe-hosts --filter 'Name=state,Values=pending,available' 'Name=tag:Name,Values=ci-d2-macos-amd64' | jq -r '.Hosts[].HostId') + if [ -z "$MACOS_AMD64_ID" ]; then + MACOS_AMD64_ID=$(sh_c aws ec2 allocate-hosts --instance-type mac1.metal --quantity 1 --availability-zone us-west-2a \ + --tag-specifications '"ResourceType=dedicated-host,Tags=[{Key=Name,Value=ci-d2-macos-amd64}]"' \ + | jq -r .HostIds[0]) + fi + + header macos-amd64 + REMOTE_NAME=ci-d2-macos-amd64 + state=$(aws ec2 describe-instances --filters \ + 'Name=instance-state-name,Values=pending,running,stopping,stopped' 'Name=tag:Name,Values=ci-d2-macos-amd64' \ + | jq -r '.Reservations[].Instances[].State.Name') + if [ -z "$state" ]; then + sh_c aws ec2 run-instances \ + --image-id=ami-0dd2ded7568750663 \ + --count=1 \ + --instance-type=mac1.metal \ + --security-groups=ssh \ + "--key-name=$KEY_NAME" \ + --iam-instance-profile 'Name=AmazonSSMRoleForInstancesQuickSetup' \ + --placement "Tenancy=host,HostId=$MACOS_AMD64_ID" \ + --block-device-mappings '"DeviceName=/dev/sda1,Ebs={VolumeSize=100,VolumeType=gp3}"' \ + --tag-specifications '"ResourceType=instance,Tags=[{Key=Name,Value=ci-d2-macos-amd64}]"' \ + '"ResourceType=volume,Tags=[{Key=Name,Value=ci-d2-macos-amd64}]"' >/dev/null + fi + wait_remote_host_ip + log "CI_D2_MACOS_AMD64=ec2-user@$ip" + export CI_D2_MACOS_AMD64=ec2-user@$ip +} + +create_macos_arm64() { + header macos-arm64-host + MACOS_ARM64_ID=$(aws ec2 describe-hosts --filter 'Name=state,Values=pending,available' 'Name=tag:Name,Values=ci-d2-macos-arm64' | jq -r '.Hosts[].HostId') + if [ -z "$MACOS_ARM64_ID" ]; then + MACOS_ARM64_ID=$(sh_c aws ec2 allocate-hosts --instance-type mac2.metal --quantity 1 --availability-zone us-west-2a \ + --tag-specifications '"ResourceType=dedicated-host,Tags=[{Key=Name,Value=ci-d2-macos-arm64}]"' \ + | jq -r .HostIds[0]) + fi + + header macos-arm64 + REMOTE_NAME=ci-d2-macos-arm64 + state=$(aws ec2 describe-instances --filters \ + 'Name=instance-state-name,Values=pending,running,stopping,stopped' 'Name=tag:Name,Values=ci-d2-macos-arm64' \ + | jq -r '.Reservations[].Instances[].State.Name') + if [ -z "$state" ]; then + sh_c aws ec2 run-instances \ + --image-id=ami-0af0516ff2c43dbbe \ + --count=1 \ + --instance-type=mac2.metal \ + --security-groups=ssh \ + "--key-name=$KEY_NAME" \ + --iam-instance-profile 'Name=AmazonSSMRoleForInstancesQuickSetup' \ + --placement "Tenancy=host,HostId=$MACOS_ARM64_ID" \ + --block-device-mappings '"DeviceName=/dev/sda1,Ebs={VolumeSize=100,VolumeType=gp3}"' \ + --tag-specifications '"ResourceType=instance,Tags=[{Key=Name,Value=ci-d2-macos-arm64}]"' \ + '"ResourceType=volume,Tags=[{Key=Name,Value=ci-d2-macos-arm64}]"' >/dev/null + fi + wait_remote_host_ip + log "CI_D2_MACOS_ARM64=ec2-user@$ip" + export CI_D2_MACOS_ARM64=ec2-user@$ip +} + +create_windows_amd64() { + header windows-amd64 + REMOTE_NAME=ci-d2-windows-amd64 + state=$(aws ec2 describe-instances --filters \ + 'Name=instance-state-name,Values=pending,running,stopping,stopped' "Name=tag:Name,Values=$REMOTE_NAME" \ + | jq -r '.Reservations[].Instances[].State.Name') + if [ -z "$state" ]; then + sh_c aws ec2 run-instances \ + --image-id=ami-0c5300e833c2b32f3 \ + --count=1 \ + --instance-type=t3.medium \ + --security-groups=windows \ + "--key-name=$KEY_NAME_WINDOWS" \ + --iam-instance-profile 'Name=AmazonSSMRoleForInstancesQuickSetup' \ + --block-device-mappings '"DeviceName=/dev/sda1,Ebs={VolumeSize=64,VolumeType=gp3}"' \ + --tag-specifications "'ResourceType=instance,Tags=[{Key=Name,Value=$REMOTE_NAME}]'" \ + "'ResourceType=volume,Tags=[{Key=Name,Value=$REMOTE_NAME}]'" >/dev/null + fi + wait_remote_host_ip + log "CI_D2_WINDOWS_AMD64=Administrator@$ip" + export CI_D2_WINDOWS_AMD64=Administrator@$ip +} + +wait_remote_host_ip() { + while true; do + ip=$(sh_c aws ec2 describe-instances \ + --filters 'Name=instance-state-name,Values=pending,running,stopping,stopped' "Name=tag:Name,Values=$REMOTE_NAME" \ + | jq -r '.Reservations[].Instances[].PublicIpAddress') + if [ -n "$ip" ]; then + alloc_static_ip + ip=$(sh_c aws ec2 describe-instances \ + --filters 'Name=instance-state-name,Values=pending,running,stopping,stopped' "Name=tag:Name,Values=$REMOTE_NAME" \ + | jq -r '.Reservations[].Instances[].PublicIpAddress') + ssh-keygen -R "$ip" + break + fi + sleep 5 + done +} + +alloc_static_ip() { + allocation_id=$(aws ec2 describe-addresses --filters "Name=tag:Name,Values=$REMOTE_NAME" | jq -r '.Addresses[].AllocationId') + if [ -z "$allocation_id" ]; then + sh_c aws ec2 allocate-address --tag-specifications "'ResourceType=elastic-ip,Tags=[{Key=Name,Value=$REMOTE_NAME}]'" + allocation_id=$(aws ec2 describe-addresses --filters "Name=tag:Name,Values=$REMOTE_NAME" | jq -r '.Addresses[].AllocationId') + fi + + instance_id=$(aws ec2 describe-instances \ + --filters 'Name=instance-state-name,Values=pending,running,stopping,stopped' "Name=tag:Name,Values=$REMOTE_NAME" \ + | jq -r '.Reservations[].Instances[].InstanceId') + aws ec2 associate-address --instance-id "$instance_id" --allocation-id "$allocation_id" +} + +init_remote_hosts() { + bigheader init_remote_hosts + + JOBNAME=$JOBNAME/linux/amd64 runjob_filter REMOTE_HOST=$CI_D2_LINUX_AMD64 REMOTE_NAME=ci-d2-linux-amd64 init_remote_linux + JOBNAME=$JOBNAME/linux/arm64 runjob_filter REMOTE_HOST=$CI_D2_LINUX_ARM64 REMOTE_NAME=ci-d2-linux-arm64 init_remote_linux + JOBNAME=$JOBNAME/macos/amd64 runjob_filter REMOTE_HOST=$CI_D2_MACOS_AMD64 REMOTE_NAME=ci-d2-macos-amd64 init_remote_macos + JOBNAME=$JOBNAME/macos/arm64 runjob_filter REMOTE_HOST=$CI_D2_MACOS_ARM64 REMOTE_NAME=ci-d2-macos-arm64 init_remote_macos + JOBNAME=$JOBNAME/windows/amd64 runjob_filter REMOTE_HOST=$CI_D2_WINDOWS_AMD64 REMOTE_NAME=ci-d2-windows-amd64 init_remote_windows + + # Windows and AWS SSM both defeated me. + FGCOLOR=3 bigheader "WARNING: WINDOWS INITIALIZATION MUST BE COMPLETED MANUALLY OVER RDP AND POWERSHELL!" +} + +init_remote_linux() { + header "$REMOTE_NAME" + wait_remote_host + + sh_c ssh_copy_id -i="$ID_PUB_PATH" "$REMOTE_HOST" + + sh_c ssh "$REMOTE_HOST" sh -s -- < /dev/null +sudo -E apt-get update -y +sudo -E apt-get install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin +sudo groupadd docker || true +sudo usermod -aG docker \$USER + +mkdir -p \$HOME/.local/bin +mkdir -p \$HOME/.local/share/man +EOF + init_remote_env + + sh_c ssh "$REMOTE_HOST" sh -s -- <> ~/.zshrc\"" + fi + if ! sh_c ssh "$REMOTE_HOST" "'grep -qF \\\$HOME/.local ~/.zshrc'"; then + sh_c ssh "$REMOTE_HOST" "\"(echo && cat) >> ~/.zshrc\"" <\$HOME/.ssh/environment"' + sh_c ssh "$REMOTE_HOST" '"echo MANPATH=\$(echo \"echo \\\$MANPATH\" | \"\$SHELL\" -ils) >>\$HOME/.ssh/environment"' + + sh_c ssh "$REMOTE_HOST" "sudo sed -i.bak '\"s/#PermitUserEnvironment no/PermitUserEnvironment yes/\"' /etc/ssh/sshd_config" + + if sh_c ssh "$REMOTE_HOST" uname | grep -qF Darwin; then + sh_c ssh "$REMOTE_HOST" "sudo launchctl stop com.openssh.sshd" + else + sh_c ssh "$REMOTE_HOST" "sudo systemctl restart sshd" + # ubuntu has $PATH hard coded in /etc/environment for some reason. It takes precedence + # over ~/.ssh/environment. + sh_c ssh "$REMOTE_HOST" "sudo rm /etc/environment" + fi +} + +wait_remote_host() { + while true; do + if sh_c ssh "$REMOTE_HOST" true; then + break + fi + sleep 5 + done +} + +wait_remote_host_windows() { + instance_id=$(aws ec2 describe-instances \ + --filters 'Name=instance-state-name,Values=pending,running,stopping,stopped' "Name=tag:Name,Values=$REMOTE_NAME" \ + | jq -r '.Reservations[].Instances[].InstanceId') + + while true; do + if sh_c aws ssm start-session --target "$instance_id" \ + --document-name 'AWS-StartNonInteractiveCommand' \ + --parameters "'{\"command\": [\"echo true\"]}'"; then + break + fi + sleep 5 + done +} + +init_remote_windows() { + header "$REMOTE_NAME" + wait_remote_host_windows + + init_ps1=$(cat < utf8: https://stackoverflow.com/a/34969243/4283659 +\$null = New-Item -Force "\$env:ProgramData\ssh\administrators_authorized_keys" -Value (Get-Content -Path "\$env:ProgramData\ssh\administrators_authorized_keys" | Out-String) +get-acl "\$env:ProgramData\ssh\ssh_host_rsa_key" | set-acl "\$env:ProgramData\ssh\administrators_authorized_keys" + +if (-Not (Test-Path -Path C:\msys64)) { + Invoke-WebRequest -Uri "https://github.com/msys2/msys2-installer/releases/download/2022-10-28/msys2-x86_64-20221028.exe" -OutFile "./msys2-x86_64.exe" + ./msys2-x86_64.exe install --default-answer --confirm-command --root C:\msys64 +} +C:\msys64\msys2_shell.cmd -defterm -here -no-start -mingw64 -c 'pacman -Sy --noconfirm base-devel vim rsync man' +C:\msys64\msys2_shell.cmd -defterm -here -no-start -mingw64 -c 'curl -fsSL https://d2lang.com/install.sh | sh -s -- --tala' +C:\msys64\msys2_shell.cmd -defterm -here -no-start -mingw64 -c 'd2 --version' + +\$path = (Get-ItemProperty -Path 'Registry::HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\Session Manager\Environment' -Name Path).Path +if (\$path -notlike '*C:\msys64\usr\bin*') { + \$path = "\$path;C:\msys64\usr\bin" + Set-ItemProperty -Path 'Registry::HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\Session Manager\Environment' -Name Path -Value \$path +} +if (\$path -notlike '*C:\msys64\usr\local\bin*') { + \$path = "\$path;C:\msys64\usr\local\bin" + Set-ItemProperty -Path 'Registry::HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\Session Manager\Environment' -Name Path -Value \$path +} +(Get-ItemProperty -Path 'Registry::HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\Session Manager\Environment' -Name Path).Path + +Restart-Computer +EOF + +# To run a POSIX script: +# ssh "$CI_D2_WINDOWS_AMD64" sh -s -- < utf8: https://stackoverflow.com/a/34969243/4283659 +# \$null = New-Item -Force C:\msys64\sshd_default_shell.cmd -Value (Get-Content -Path C:\msys64\sshd_default_shell.cmd | Out-String) +# Set-ItemProperty -Path HKLM:\SOFTWARE\OpenSSH -Name DefaultShell -Value C:\msys64\sshd_default_shell.cmd +# EOF +# +# To undo: +# <&2 + warn "2. RDP into $REMOTE_HOST and open PowerShell." + warn '3. Generate and execute C:\Users\Administrator\Desktop\init.ps1 with:' + printf '%s\n' "$gen_init_ps1" >&2 + warn '4. Run the following to be notified once installation is successful:' + cat <$rsync_files + sh_c rsync --archive --human-readable --files-from "$rsync_files" "'$REMOTE_HOST:windows\\'" "./ci/release/build/$VERSION/d2-$VERSION-$OS-$ARCH.msi" } main "$@" diff --git a/ci/release/build/.dockerignore b/ci/release/build/.dockerignore new file mode 100644 index 000000000..73f0caa06 --- /dev/null +++ b/ci/release/build/.dockerignore @@ -0,0 +1 @@ +**/d2*/ diff --git a/ci/release/build_docker.sh b/ci/release/build_in_docker.sh similarity index 81% rename from ci/release/build_docker.sh rename to ci/release/build_in_docker.sh index 2939c5dd0..b8a4f1f6c 100755 --- a/ci/release/build_docker.sh +++ b/ci/release/build_in_docker.sh @@ -5,7 +5,7 @@ cd -- "$(dirname "$0")/../.." tag="$(sh_c docker build \ --build-arg GOVERSION="1.19.3.linux-$ARCH" \ - -qf ./ci/release/builders/Dockerfile ./ci/release/builders)" + -qf ./ci/release/linux/Dockerfile ./ci/release/linux)" docker_run \ -e DRY_RUN \ -e HW_BUILD_DIR \ diff --git a/ci/release/builders/Dockerfile-centos b/ci/release/builders/Dockerfile-centos deleted file mode 100644 index 66aaf6f17..000000000 --- a/ci/release/builders/Dockerfile-centos +++ /dev/null @@ -1,16 +0,0 @@ -FROM centos:7 - -ARG GOVERSION= - -RUN curl -fsSL "https://go.dev/dl/go$GOVERSION.tar.gz" >/tmp/go.tar.gz -RUN tar -C /usr/local -xzf /tmp/go.tar.gz - -ENV PATH="/usr/local/go/bin:$PATH" - -RUN yum install -y rsync wget -RUN yum groupinstall -y 'Development Tools' - -RUN curl -fsSL https://ftp.gnu.org/gnu/gcc/gcc-5.2.0/gcc-5.2.0.tar.gz >/tmp/gcc.tar.gz -RUN tar -C /usr/local -xzf /tmp/gcc.tar.gz -RUN cd /usr/local/gcc-5.2.0 && ./contrib/download_prerequisites && mkdir -p build \ - && cd build && ../configure --disable-multilib && make && make install diff --git a/ci/release/builders/aws_copy_keys.sh b/ci/release/builders/aws_copy_keys.sh deleted file mode 100755 index cc0d548ef..000000000 --- a/ci/release/builders/aws_copy_keys.sh +++ /dev/null @@ -1,57 +0,0 @@ -#!/bin/sh -set -eu -cd -- "$(dirname "$0")/../../.." -. ./ci/sub/lib.sh - -help() { - cat < .ssh/authorized_keys.dedup' - sh_c ssh "$REMOTE_HOST" 'cp .ssh/authorized_keys.dedup .ssh/authorized_keys' - sh_c ssh "$REMOTE_HOST" 'rm .ssh/authorized_keys.dedup' -} - -main "$@" diff --git a/ci/release/builders/aws_ensure.sh b/ci/release/builders/aws_ensure.sh deleted file mode 100755 index 63c117abc..000000000 --- a/ci/release/builders/aws_ensure.sh +++ /dev/null @@ -1,294 +0,0 @@ -#!/bin/sh -set -eu -cd -- "$(dirname "$0")/../../.." -. ./ci/sub/lib.sh - -help() { - cat </dev/null \ - | jq -r .SecurityGroups[0].GroupId) - if [ -z "$SG_ID" ]; then - SG_ID=$(sh_c aws ec2 create-security-group \ - --group-name ssh \ - --description ssh \ - --vpc-id "$VPC_ID" | jq -r .GroupId) - fi - - header security-group-ingress - SG_RULES_COUNT=$(aws ec2 describe-security-groups --group-names ssh \ - | jq -r '.SecurityGroups[0].IpPermissions | length') - if [ "$SG_RULES_COUNT" -eq 0 ]; then - sh_c aws ec2 authorize-security-group-ingress \ - --group-id "$SG_ID" \ - --protocol tcp \ - --port 22 \ - --cidr 0.0.0.0/0 >/dev/null - fi - - header linux-amd64 - state=$(aws ec2 describe-instances --filters \ - 'Name=instance-state-name,Values=pending,running,stopping,stopped' 'Name=tag:Name,Values=d2-builder-linux-amd64' \ - | jq -r '.Reservations[].Instances[].State.Name') - if [ -z "$state" ]; then - sh_c aws ec2 run-instances \ - --image-id=ami-071e6cafc48327ca2 \ - --count=1 \ - --instance-type=t2.small \ - --security-groups=ssh \ - "--key-name=$KEY_NAME" \ - --tag-specifications '"ResourceType=instance,Tags=[{Key=Name,Value=d2-builder-linux-amd64}]"' \ - '"ResourceType=volume,Tags=[{Key=Name,Value=d2-builder-linux-amd64}]"' >/dev/null - fi - while true; do - dnsname=$(sh_c aws ec2 describe-instances \ - --filters 'Name=instance-state-name,Values=pending,running,stopping,stopped' 'Name=tag:Name,Values=d2-builder-linux-amd64' \ - | jq -r '.Reservations[].Instances[].PublicDnsName') - if [ -n "$dnsname" ]; then - log "TSTRUCT_LINUX_AMD64_BUILDER=admin@$dnsname" - export TSTRUCT_LINUX_AMD64_BUILDER=admin@$dnsname - break - fi - sleep 5 - done - - header linux-arm64 - state=$(aws ec2 describe-instances --filters \ - 'Name=instance-state-name,Values=pending,running,stopping,stopped' 'Name=tag:Name,Values=d2-builder-linux-arm64' \ - | jq -r '.Reservations[].Instances[].State.Name') - if [ -z "$state" ]; then - sh_c aws ec2 run-instances \ - --image-id=ami-0e67506f183e5ab60 \ - --count=1 \ - --instance-type=t4g.small \ - --security-groups=ssh \ - "--key-name=$KEY_NAME" \ - --tag-specifications '"ResourceType=instance,Tags=[{Key=Name,Value=d2-builder-linux-arm64}]"' \ - '"ResourceType=volume,Tags=[{Key=Name,Value=d2-builder-linux-arm64}]"' >/dev/null - fi - while true; do - dnsname=$(sh_c aws ec2 describe-instances \ - --filters 'Name=instance-state-name,Values=pending,running,stopping,stopped' 'Name=tag:Name,Values=d2-builder-linux-arm64' \ - | jq -r '.Reservations[].Instances[].PublicDnsName') - if [ -n "$dnsname" ]; then - log "TSTRUCT_LINUX_ARM64_BUILDER=admin@$dnsname" - export TSTRUCT_LINUX_ARM64_BUILDER=admin@$dnsname - break - fi - sleep 5 - done - - header "macos-amd64-host" - MACOS_AMD64_HOST_ID=$(aws ec2 describe-hosts --filter 'Name=state,Values=pending,available' 'Name=tag:Name,Values=d2-builder-macos-amd64' | jq -r '.Hosts[].HostId') - if [ -z "$MACOS_AMD64_HOST_ID" ]; then - MACOS_AMD64_HOST_ID=$(sh_c aws ec2 allocate-hosts --instance-type mac1.metal --quantity 1 --availability-zone us-west-2a \ - --tag-specifications '"ResourceType=dedicated-host,Tags=[{Key=Name,Value=d2-builder-macos-amd64}]"' \ - | jq -r .HostIds[0]) - fi - - header "macos-arm64-host" - MACOS_ARM64_HOST_ID=$(aws ec2 describe-hosts --filter 'Name=state,Values=pending,available' 'Name=tag:Name,Values=d2-builder-macos-arm64' | jq -r '.Hosts[].HostId') - if [ -z "$MACOS_ARM64_HOST_ID" ]; then - MACOS_ARM64_HOST_ID=$(sh_c aws ec2 allocate-hosts --instance-type mac2.metal --quantity 1 --availability-zone us-west-2a \ - --tag-specifications '"ResourceType=dedicated-host,Tags=[{Key=Name,Value=d2-builder-macos-amd64}]"' \ - | jq -r .HostIds[0]) - fi - - header macos-amd64 - state=$(aws ec2 describe-instances --filters \ - 'Name=instance-state-name,Values=pending,running,stopping,stopped' 'Name=tag:Name,Values=d2-builder-macos-amd64' \ - | jq -r '.Reservations[].Instances[].State.Name') - if [ -z "$state" ]; then - sh_c aws ec2 run-instances \ - --image-id=ami-0dd2ded7568750663 \ - --count=1 \ - --instance-type=mac1.metal \ - --security-groups=ssh \ - "--key-name=$KEY_NAME" \ - --placement "Tenancy=host,HostId=$MACOS_AMD64_HOST_ID" \ - --tag-specifications '"ResourceType=instance,Tags=[{Key=Name,Value=d2-builder-macos-amd64}]"' \ - '"ResourceType=volume,Tags=[{Key=Name,Value=d2-builder-macos-amd64}]"' >/dev/null - fi - while true; do - dnsname=$(sh_c aws ec2 describe-instances \ - --filters 'Name=instance-state-name,Values=pending,running,stopping,stopped' 'Name=tag:Name,Values=d2-builder-macos-amd64' \ - | jq -r '.Reservations[].Instances[].PublicDnsName') - if [ -n "$dnsname" ]; then - log "TSTRUCT_MACOS_AMD64_BUILDER=ec2-user@$dnsname" - export TSTRUCT_MACOS_AMD64_BUILDER=ec2-user@$dnsname - break - fi - sleep 5 - done - - header macos-arm64 - state=$(aws ec2 describe-instances --filters \ - 'Name=instance-state-name,Values=pending,running,stopping,stopped' 'Name=tag:Name,Values=d2-builder-macos-arm64' \ - | jq -r '.Reservations[].Instances[].State.Name') - if [ -z "$state" ]; then - sh_c aws ec2 run-instances \ - --image-id=ami-0af0516ff2c43dbbe \ - --count=1 \ - --instance-type=mac2.metal \ - --security-groups=ssh \ - "--key-name=$KEY_NAME" \ - --placement "Tenancy=host,HostId=$MACOS_ARM64_HOST_ID" \ - --tag-specifications '"ResourceType=instance,Tags=[{Key=Name,Value=d2-builder-macos-arm64}]"' \ - '"ResourceType=volume,Tags=[{Key=Name,Value=d2-builder-macos-arm64}]"' >/dev/null - fi - while true; do - dnsname=$(sh_c aws ec2 describe-instances \ - --filters 'Name=instance-state-name,Values=pending,running,stopping,stopped' 'Name=tag:Name,Values=d2-builder-macos-arm64' \ - | jq -r '.Reservations[].Instances[].PublicDnsName') - if [ -n "$dnsname" ]; then - log "TSTRUCT_MACOS_ARM64_BUILDER=ec2-user@$dnsname" - export TSTRUCT_MACOS_ARM64_BUILDER=ec2-user@$dnsname - break - fi - sleep 5 - done -} - -init_remote_hosts() { - header linux-amd64 - REMOTE_HOST=$TSTRUCT_LINUX_AMD64_BUILDER init_remote_linux - header linux-arm64 - REMOTE_HOST=$TSTRUCT_LINUX_ARM64_BUILDER init_remote_linux - header macos-amd64 - REMOTE_HOST=$TSTRUCT_MACOS_AMD64_BUILDER init_remote_macos - header macos-arm64 - REMOTE_HOST=$TSTRUCT_MACOS_ARM64_BUILDER init_remote_macos - - 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" - log "export TSTRUCT_MACOS_ARM64_BUILDER=$TSTRUCT_MACOS_ARM64_BUILDER" -} - -init_remote_linux() { - wait_remote_host - - sh_c ssh "$REMOTE_HOST" sh -s -- < /dev/null -sudo -E apt-get update -y -sudo -E apt-get install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin -sudo groupadd docker || true -sudo usermod -aG docker \$USER - -mkdir -p \$HOME/.local/bin -mkdir -p \$HOME/.local/share/man -EOF - init_remote_env - - sh_c ssh "$REMOTE_HOST" 'sudo reboot' || true -} - -init_remote_macos() { - wait_remote_host - - sh_c ssh "$REMOTE_HOST" '"/bin/bash -c \"\$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)\""' - - if sh_c ssh "$REMOTE_HOST" uname -m | grep -qF arm64; then - shellenv=$(sh_c ssh "$REMOTE_HOST" /opt/homebrew/bin/brew shellenv) - else - shellenv=$(sh_c ssh "$REMOTE_HOST" /usr/local/bin/brew shellenv) - fi - if ! echo "$shellenv" | sh_c ssh "$REMOTE_HOST" "IFS= read -r regex\; \"grep -qF \\\"\\\$regex\\\" ~/.zshrc\""; then - echo "$shellenv" | sh_c ssh "$REMOTE_HOST" "\"(echo && cat) >> ~/.zshrc\"" - fi - if ! sh_c ssh "$REMOTE_HOST" "'grep -qF \\\$HOME/.local ~/.zshrc'"; then - sh_c ssh "$REMOTE_HOST" "\"(echo && cat) >> ~/.zshrc\"" <\$HOME/.ssh/environment"' - sh_c ssh "$REMOTE_HOST" '"echo MANPATH=\$(echo \"echo \\\$MANPATH\" | \"\$SHELL\" -ils) >>\$HOME/.ssh/environment"' - - sh_c ssh "$REMOTE_HOST" "sudo sed -i.bak '\"s/#PermitUserEnvironment no/PermitUserEnvironment yes/\"' /etc/ssh/sshd_config" - - if sh_c ssh "$REMOTE_HOST" uname | grep -qF Darwin; then - sh_c ssh "$REMOTE_HOST" "sudo launchctl stop com.openssh.sshd" - else - sh_c ssh "$REMOTE_HOST" "sudo systemctl restart sshd" - fi -} - -wait_remote_host() { - while true; do - if sh_c ssh "$REMOTE_HOST" true; then - break - fi - sleep 5 - done -} - -main "$@" diff --git a/ci/release/changelogs/next.md b/ci/release/changelogs/next.md index 10392f641..ff0cbf2da 100644 --- a/ci/release/changelogs/next.md +++ b/ci/release/changelogs/next.md @@ -1,21 +1,19 @@ #### Features ๐Ÿš€ -- Diagram padding can now can be configured in the CLI (default 100px). - [https://github.com/terrastruct/d2/pull/431](https://github.com/terrastruct/d2/pull/431) +- Diagram padding can now can be configured in the CLI (default 100px). [https://github.com/terrastruct/d2/pull/431](https://github.com/terrastruct/d2/pull/431) - Connection label backgrounds can now be set with the `style.fill` keyword. [https://github.com/terrastruct/d2/pull/452](https://github.com/terrastruct/d2/pull/452) +- Add official Docker image. See [./docs/INSTALL.md#docker](./docs/INSTALL.md#docker). [#76](https://github.com/terrastruct/d2/issues/76) +- Add `.msi` installer for convenient installation on Windows. [#379](https://github.com/terrastruct/d2/issues/379) #### Improvements ๐Ÿงน -- Fmt now preserves leading comment spacing. - [#400](https://github.com/terrastruct/d2/issues/400) +- `d2 fmt` now preserves leading comment spacing. [#400](https://github.com/terrastruct/d2/issues/400) #### Bugfixes โ›‘๏ธ -- Fixed crash when sequence diagrams had no messages. - [https://github.com/terrastruct/d2/pull/427](https://github.com/terrastruct/d2/pull/427) -- Fixed `constraint` keyword setting label. - [https://github.com/terrastruct/d2/issues/415](https://github.com/terrastruct/d2/issues/415) -- Fixed serialization affecting binary plugins (TALA). - [https://github.com/terrastruct/d2/pull/426](https://github.com/terrastruct/d2/pull/426) +- Fixed crash when sequence diagrams had no messages. [https://github.com/terrastruct/d2/pull/427](https://github.com/terrastruct/d2/pull/427) +- Fixed `constraint` keyword setting label. [https://github.com/terrastruct/d2/issues/415](https://github.com/terrastruct/d2/issues/415) +- Fixed serialization affecting binary plugins (TALA). [https://github.com/terrastruct/d2/pull/426](https://github.com/terrastruct/d2/pull/426) - Fixed connections in elk layouts not going all the way to shape borders. [https://github.com/terrastruct/d2/pull/459](https://github.com/terrastruct/d2/pull/459) - Fixed a connection rendering bug that could happen in firefox when there were no connection labels. [https://github.com/terrastruct/d2/pull/453](https://github.com/terrastruct/d2/pull/453) +- Fixed a crash when external connection IDs were prefixes of a sequence diagram ID. [https://github.com/terrastruct/d2/pull/462](https://github.com/terrastruct/d2/pull/462) diff --git a/ci/release/builders/Dockerfile b/ci/release/linux/Dockerfile similarity index 77% rename from ci/release/builders/Dockerfile rename to ci/release/linux/Dockerfile index 5adc0bd25..5bc66a568 100644 --- a/ci/release/builders/Dockerfile +++ b/ci/release/linux/Dockerfile @@ -8,5 +8,5 @@ RUN curl -fsSL "https://go.dev/dl/go$GOVERSION.tar.gz" >/tmp/go.tar.gz RUN tar -C /usr/local -xzf /tmp/go.tar.gz ENV PATH="/usr/local/go/bin:$PATH" -RUN apt-get install -y build-essential -RUN apt-get install -y rsync +RUN apt-get install -y build-essential \ + rsync diff --git a/ci/release/template/README.md.sh b/ci/release/template/README.md.sh index 7ccea6fb2..d08a71bf6 100755 --- a/ci/release/template/README.md.sh +++ b/ci/release/template/README.md.sh @@ -20,27 +20,11 @@ EOF if [ "$OS" = windows ]; then cat <\` - -For installation you'll have to add the \`./bin/d2.exe\` binary to your \`\$PATH\`. Or add -the \`./bin\` directory of this release to your \`\$PATH\`. - -See https://www.wikihow.com/Change-the-PATH-Environment-Variable-on-Windows - -Then you'll be able to call \`d2\` from the commandline in \`cmd.exe\` or \`pwsh.exe\`. - -We intend to have a \`.msi\` release installer sometime soon that handles putting \`d2.exe\` into +You may find our \`.msi\` installer more convenient as it handles putting \`d2.exe\` into your \`\$PATH\` for you. -You can also use \`make install\` to install on Windows after first installing -[MSYS2](https://www.msys2.org/) which emulates a Linux shell for Windows. Its terminal -also enables \`d2\` to show colors in its output. The manpage will also become accessible -with \`man d2\`. - See https://github.com/terrastruct/d2/blob/master/docs/INSTALL.md#windows EOF fi diff --git a/ci/release/template/scripts/lib.sh b/ci/release/template/scripts/lib.sh index 62ade2f89..699795908 100644 --- a/ci/release/template/scripts/lib.sh +++ b/ci/release/template/scripts/lib.sh @@ -273,7 +273,7 @@ header() { bigheader() { set -- "$(echo "$*" | sed "s/^/ * /")" - FGCOLOR=${FGCOLOR:-3} logp "/**************************************************************** + FGCOLOR=${FGCOLOR:-6} logp "/**************************************************************** $* ****************************************************************/" } diff --git a/ci/release/windows/.gitignore b/ci/release/windows/.gitignore new file mode 100644 index 000000000..55ae023fc --- /dev/null +++ b/ci/release/windows/.gitignore @@ -0,0 +1 @@ +d2.exe diff --git a/ci/release/windows/d2.ico b/ci/release/windows/d2.ico new file mode 100644 index 000000000..bc8335225 Binary files /dev/null and b/ci/release/windows/d2.ico differ diff --git a/ci/release/windows/d2.png b/ci/release/windows/d2.png new file mode 100644 index 000000000..ba83fed7a Binary files /dev/null and b/ci/release/windows/d2.png differ diff --git a/ci/release/windows/d2.wxs b/ci/release/windows/d2.wxs new file mode 100644 index 000000000..1b7b46631 --- /dev/null +++ b/ci/release/windows/d2.wxs @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ci/sub b/ci/sub index 7923b353a..2db232e4a 160000 --- a/ci/sub +++ b/ci/sub @@ -1 +1 @@ -Subproject commit 7923b353a829cf289171baff0d0d017ae3d32278 +Subproject commit 2db232e4aa5225e22f95ff91914f91b59fe48d07 diff --git a/d2layouts/d2sequence/layout.go b/d2layouts/d2sequence/layout.go index 42efeb174..55192ceab 100644 --- a/d2layouts/d2sequence/layout.go +++ b/d2layouts/d2sequence/layout.go @@ -88,7 +88,7 @@ func layoutSequenceDiagram(g *d2graph.Graph, obj *d2graph.Object) (*sequenceDiag var edges []*d2graph.Edge for _, edge := range g.Edges { // both Src and Dst must be inside the sequence diagram - if strings.HasPrefix(edge.Src.AbsID(), obj.AbsID()) && strings.HasPrefix(edge.Dst.AbsID(), obj.AbsID()) { + if obj == g.Root || (strings.HasPrefix(edge.Src.AbsID(), obj.AbsID()+".") && strings.HasPrefix(edge.Dst.AbsID(), obj.AbsID()+".")) { edges = append(edges, edge) } } diff --git a/d2layouts/d2sequence/sequence_diagram.go b/d2layouts/d2sequence/sequence_diagram.go index b05ea10a6..258428975 100644 --- a/d2layouts/d2sequence/sequence_diagram.go +++ b/d2layouts/d2sequence/sequence_diagram.go @@ -466,8 +466,8 @@ func (sd *sequenceDiagram) routeMessages() error { } else { return fmt.Errorf("could not find center of %s", message.Dst.AbsID()) } - isToDescendant := strings.HasPrefix(message.Dst.AbsID(), message.Src.AbsID()) - isFromDescendant := strings.HasPrefix(message.Src.AbsID(), message.Dst.AbsID()) + isToDescendant := strings.HasPrefix(message.Dst.AbsID(), message.Src.AbsID()+".") + isFromDescendant := strings.HasPrefix(message.Src.AbsID(), message.Dst.AbsID()+".") isSelfMessage := message.Src == message.Dst if isSelfMessage || isToDescendant || isFromDescendant { diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index f5b08f6b1..801917b2b 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -1,19 +1,14 @@ # Contributing - -- [CI](#ci) -- [Flow](#flow) -- [Logistics](#logistics) -- [Dev](#dev) - * [Content](#content) - * [Tests](#tests) - + [Running tests](#running-tests) - + [Chaos tests](#chaos-tests) - * [Documentation](#documentation) - * [Questions](#questions) - - +- CI +- Flow +- Logistics +- Dev + - Content + - Tests + - Documentation + - Questions ## CI diff --git a/docs/INSTALL.md b/docs/INSTALL.md index 30e31d7cd..410c95f77 100644 --- a/docs/INSTALL.md +++ b/docs/INSTALL.md @@ -14,8 +14,9 @@ You may install `d2` through any of the following methods. - From source - Source Release - Windows - - MSYS2 + - Release archives - WSL +- Docker - Coming soon ## install.sh @@ -157,6 +158,8 @@ You can always install from source: go install oss.terrastruct.com/d2@latest ``` +You need at least Go v1.18 + ### Source Release To install a release from source clone the repository and then: @@ -172,33 +175,23 @@ fonts and icons. Furthermore, when installing a non versioned commit, installing will ensure that `d2 --version` works correctly by embedding the commit hash into the `d2` binary. +Remember, you need at least Go v1.18 + ## Windows -d2 builds and runs on Windows: +We have prebuilt releases of d2 available for Windows via `.msi` installers. The installer +will add the `d2` binary to your `$PATH` so that you can execute `d2` in `cmd.exe` or +`pwsh.exe`. -We have prebuilt standalone releases for Windows though they're structured in the same way -as our Unix releases. +### Release archives -Easiest way to use d2 on Windows is to just `chdir` into the bin directory of the release -and invoke d2 like `./d2 ` - -For installation, you'll have to put the `bin/d2.exe` binary into your `$PATH` or add the -`bin` directory of the release into your `$PATH`. - -See https://www.wikihow.com/Change-the-PATH-Environment-Variable-on-Windows - -Then you'll be able to call `d2` from the commandline in `cmd.exe` or `pwsh.exe`. - -We intend to have a `.msi` release installer sometime soon that handles putting `d2` into -your `$PATH` for you. - -### MSYS2 +We also have release archives for Windows structured in the same way as our Unix releases +for use with MSYS2. Screenshot 2022-12-06 at 2 55 27 AM -We recommend using [MSYS2](https://www.msys2.org/) or [Git -Bash](https://gitforwindows.org/#bash) (Git Bash is based on MSYS2) for an improved -terminal experience. +See [MSYS2](https://www.msys2.org/) or [Git Bash](https://gitforwindows.org/#bash) (Git +Bash is based on MSYS2). MSYS2 provides a unix style shell environment that is native to Windows (unlike [Cygwin](https://www.cygwin.com/)). MSYS2 allows `install.sh` to work, enables automatic @@ -216,9 +209,23 @@ under plain Windows. aka Windows Subsystem for Linux if that's what you prefer. Installation is just like any other Linux system. +## Docker + +https://hub.docker.com/repository/docker/terrastruct/d2 + +We publish `amd64` and `arm64` images based on `debian:latest` for each release. + +Example usage: + +```sh +echo 'x -> y' >helloworld.d2 +docker run --rm -it -u "$(id -u):$(id -g)" -v "$PWD:/root/src" \ + -p 127.0.0.1:8080:8080 terrastruct/d2 --watch helloworld.d2 +# Visit http://127.0.0.1:8080 +``` + ## Coming soon -- Docker image - rpm and deb packages - with repositories and standalone - homebrew core diff --git a/e2etests/regression_test.go b/e2etests/regression_test.go index 7c627be74..b9c2399f1 100644 --- a/e2etests/regression_test.go +++ b/e2etests/regression_test.go @@ -40,6 +40,18 @@ b.1 -> b.1`, a: A b: B`, }, + { + name: "sequence_diagram_name_crash", + script: `foo: { + shape: sequence_diagram + a -> b +} +foobar: { + shape: sequence_diagram + c -> d +} +foo -> foobar`, + }, } runa(t, tcs) diff --git a/e2etests/testdata/regression/sequence_diagram_name_crash/dagre/board.exp.json b/e2etests/testdata/regression/sequence_diagram_name_crash/dagre/board.exp.json new file mode 100644 index 000000000..c07f75069 --- /dev/null +++ b/e2etests/testdata/regression/sequence_diagram_name_crash/dagre/board.exp.json @@ -0,0 +1,529 @@ +{ + "name": "", + "shapes": [ + { + "id": "foo", + "type": "sequence_diagram", + "pos": { + "x": 0, + "y": 0 + }, + "width": 448, + "height": 520, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 0, + "borderRadius": 0, + "fill": "#FFFFFF", + "stroke": "#0D32B2", + "shadow": false, + "3d": false, + "multiple": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "blend": false, + "fields": null, + "methods": null, + "columns": null, + "label": "foo", + "fontSize": 28, + "fontFamily": "DEFAULT", + "language": "", + "color": "#0A0F25", + "italic": false, + "bold": false, + "underline": false, + "labelWidth": 43, + "labelHeight": 41, + "labelPosition": "INSIDE_TOP_CENTER", + "zIndex": 0, + "level": 1 + }, + { + "id": "foobar", + "type": "sequence_diagram", + "pos": { + "x": 0, + "y": 620 + }, + "width": 448, + "height": 520, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 0, + "borderRadius": 0, + "fill": "#FFFFFF", + "stroke": "#0D32B2", + "shadow": false, + "3d": false, + "multiple": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "blend": false, + "fields": null, + "methods": null, + "columns": null, + "label": "foobar", + "fontSize": 28, + "fontFamily": "DEFAULT", + "language": "", + "color": "#0A0F25", + "italic": false, + "bold": false, + "underline": false, + "labelWidth": 84, + "labelHeight": 41, + "labelPosition": "INSIDE_TOP_CENTER", + "zIndex": 0, + "level": 1 + }, + { + "id": "foo.a", + "type": "", + "pos": { + "x": 24, + "y": 110 + }, + "width": 150, + "height": 126, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "borderRadius": 0, + "fill": "#EDF0FD", + "stroke": "#0D32B2", + "shadow": false, + "3d": false, + "multiple": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "blend": false, + "fields": null, + "methods": null, + "columns": null, + "label": "a", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "#0A0F25", + "italic": false, + "bold": false, + "underline": false, + "labelWidth": 12, + "labelHeight": 26, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "zIndex": 0, + "level": 2 + }, + { + "id": "foo.b", + "type": "", + "pos": { + "x": 274, + "y": 110 + }, + "width": 150, + "height": 126, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "borderRadius": 0, + "fill": "#EDF0FD", + "stroke": "#0D32B2", + "shadow": false, + "3d": false, + "multiple": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "blend": false, + "fields": null, + "methods": null, + "columns": null, + "label": "b", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "#0A0F25", + "italic": false, + "bold": false, + "underline": false, + "labelWidth": 13, + "labelHeight": 26, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "zIndex": 0, + "level": 2 + }, + { + "id": "foobar.c", + "type": "", + "pos": { + "x": 24, + "y": 730 + }, + "width": 150, + "height": 126, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "borderRadius": 0, + "fill": "#EDF0FD", + "stroke": "#0D32B2", + "shadow": false, + "3d": false, + "multiple": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "blend": false, + "fields": null, + "methods": null, + "columns": null, + "label": "c", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "#0A0F25", + "italic": false, + "bold": false, + "underline": false, + "labelWidth": 12, + "labelHeight": 26, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "zIndex": 0, + "level": 2 + }, + { + "id": "foobar.d", + "type": "", + "pos": { + "x": 274, + "y": 730 + }, + "width": 150, + "height": 126, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "borderRadius": 0, + "fill": "#EDF0FD", + "stroke": "#0D32B2", + "shadow": false, + "3d": false, + "multiple": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "blend": false, + "fields": null, + "methods": null, + "columns": null, + "label": "d", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "#0A0F25", + "italic": false, + "bold": false, + "underline": false, + "labelWidth": 13, + "labelHeight": 26, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "zIndex": 0, + "level": 2 + } + ], + "connections": [ + { + "id": "foo.(a -> b)[0]", + "src": "foo.a", + "srcArrow": "none", + "srcLabel": "", + "dst": "foo.b", + "dstArrow": "triangle", + "dstLabel": "", + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "stroke": "#0D32B2", + "label": "", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "#676C7E", + "italic": true, + "bold": false, + "underline": false, + "labelWidth": 0, + "labelHeight": 0, + "labelPosition": "", + "labelPercentage": 0, + "route": [ + { + "x": 99, + "y": 366 + }, + { + "x": 349, + "y": 366 + } + ], + "animated": false, + "tooltip": "", + "icon": null, + "zIndex": 4 + }, + { + "id": "foobar.(c -> d)[0]", + "src": "foobar.c", + "srcArrow": "none", + "srcLabel": "", + "dst": "foobar.d", + "dstArrow": "triangle", + "dstLabel": "", + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "stroke": "#0D32B2", + "label": "", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "#676C7E", + "italic": true, + "bold": false, + "underline": false, + "labelWidth": 0, + "labelHeight": 0, + "labelPosition": "", + "labelPercentage": 0, + "route": [ + { + "x": 99, + "y": 986 + }, + { + "x": 349, + "y": 986 + } + ], + "animated": false, + "tooltip": "", + "icon": null, + "zIndex": 4 + }, + { + "id": "(foo -> foobar)[0]", + "src": "foo", + "srcArrow": "none", + "srcLabel": "", + "dst": "foobar", + "dstArrow": "triangle", + "dstLabel": "", + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "stroke": "#0D32B2", + "label": "", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "#676C7E", + "italic": true, + "bold": false, + "underline": false, + "labelWidth": 0, + "labelHeight": 0, + "labelPosition": "", + "labelPercentage": 0, + "route": [ + { + "x": 224, + "y": 520 + }, + { + "x": 224, + "y": 560 + }, + { + "x": 224, + "y": 580 + }, + { + "x": 224, + "y": 620 + } + ], + "isCurve": true, + "animated": false, + "tooltip": "", + "icon": null, + "zIndex": 0 + }, + { + "id": "(foo.a -- )[0]", + "src": "foo.a", + "srcArrow": "none", + "srcLabel": "", + "dst": "a-lifeline-end-2251863791", + "dstArrow": "none", + "dstLabel": "", + "opacity": 1, + "strokeDash": 6, + "strokeWidth": 2, + "stroke": "#0D32B2", + "label": "", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "#676C7E", + "italic": true, + "bold": false, + "underline": false, + "labelWidth": 0, + "labelHeight": 0, + "labelPosition": "", + "labelPercentage": 0, + "route": [ + { + "x": 99, + "y": 236 + }, + { + "x": 99, + "y": 496 + } + ], + "animated": false, + "tooltip": "", + "icon": null, + "zIndex": 1 + }, + { + "id": "(foo.b -- )[0]", + "src": "foo.b", + "srcArrow": "none", + "srcLabel": "", + "dst": "b-lifeline-end-668380428", + "dstArrow": "none", + "dstLabel": "", + "opacity": 1, + "strokeDash": 6, + "strokeWidth": 2, + "stroke": "#0D32B2", + "label": "", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "#676C7E", + "italic": true, + "bold": false, + "underline": false, + "labelWidth": 0, + "labelHeight": 0, + "labelPosition": "", + "labelPercentage": 0, + "route": [ + { + "x": 349, + "y": 236 + }, + { + "x": 349, + "y": 496 + } + ], + "animated": false, + "tooltip": "", + "icon": null, + "zIndex": 1 + }, + { + "id": "(foobar.c -- )[0]", + "src": "foobar.c", + "srcArrow": "none", + "srcLabel": "", + "dst": "c-lifeline-end-955173837", + "dstArrow": "none", + "dstLabel": "", + "opacity": 1, + "strokeDash": 6, + "strokeWidth": 2, + "stroke": "#0D32B2", + "label": "", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "#676C7E", + "italic": true, + "bold": false, + "underline": false, + "labelWidth": 0, + "labelHeight": 0, + "labelPosition": "", + "labelPercentage": 0, + "route": [ + { + "x": 99, + "y": 856 + }, + { + "x": 99, + "y": 1116 + } + ], + "animated": false, + "tooltip": "", + "icon": null, + "zIndex": 1 + }, + { + "id": "(foobar.d -- )[0]", + "src": "foobar.d", + "srcArrow": "none", + "srcLabel": "", + "dst": "d-lifeline-end-2106864010", + "dstArrow": "none", + "dstLabel": "", + "opacity": 1, + "strokeDash": 6, + "strokeWidth": 2, + "stroke": "#0D32B2", + "label": "", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "#676C7E", + "italic": true, + "bold": false, + "underline": false, + "labelWidth": 0, + "labelHeight": 0, + "labelPosition": "", + "labelPercentage": 0, + "route": [ + { + "x": 349, + "y": 856 + }, + { + "x": 349, + "y": 1116 + } + ], + "animated": false, + "tooltip": "", + "icon": null, + "zIndex": 1 + } + ] +} diff --git a/e2etests/testdata/regression/sequence_diagram_name_crash/dagre/sketch.exp.svg b/e2etests/testdata/regression/sequence_diagram_name_crash/dagre/sketch.exp.svg new file mode 100644 index 000000000..7b7c06c7c --- /dev/null +++ b/e2etests/testdata/regression/sequence_diagram_name_crash/dagre/sketch.exp.svg @@ -0,0 +1,31 @@ + +foofoobarabcd + + + \ No newline at end of file diff --git a/e2etests/testdata/regression/sequence_diagram_name_crash/elk/board.exp.json b/e2etests/testdata/regression/sequence_diagram_name_crash/elk/board.exp.json new file mode 100644 index 000000000..49186256a --- /dev/null +++ b/e2etests/testdata/regression/sequence_diagram_name_crash/elk/board.exp.json @@ -0,0 +1,520 @@ +{ + "name": "", + "shapes": [ + { + "id": "foo", + "type": "sequence_diagram", + "pos": { + "x": 12, + "y": 12 + }, + "width": 448, + "height": 520, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 0, + "borderRadius": 0, + "fill": "#FFFFFF", + "stroke": "#0D32B2", + "shadow": false, + "3d": false, + "multiple": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "blend": false, + "fields": null, + "methods": null, + "columns": null, + "label": "foo", + "fontSize": 28, + "fontFamily": "DEFAULT", + "language": "", + "color": "#0A0F25", + "italic": false, + "bold": false, + "underline": false, + "labelWidth": 43, + "labelHeight": 41, + "labelPosition": "INSIDE_TOP_CENTER", + "zIndex": 0, + "level": 1 + }, + { + "id": "foobar", + "type": "sequence_diagram", + "pos": { + "x": 12, + "y": 632 + }, + "width": 448, + "height": 520, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 0, + "borderRadius": 0, + "fill": "#FFFFFF", + "stroke": "#0D32B2", + "shadow": false, + "3d": false, + "multiple": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "blend": false, + "fields": null, + "methods": null, + "columns": null, + "label": "foobar", + "fontSize": 28, + "fontFamily": "DEFAULT", + "language": "", + "color": "#0A0F25", + "italic": false, + "bold": false, + "underline": false, + "labelWidth": 84, + "labelHeight": 41, + "labelPosition": "INSIDE_TOP_CENTER", + "zIndex": 0, + "level": 1 + }, + { + "id": "foo.a", + "type": "", + "pos": { + "x": 36, + "y": 122 + }, + "width": 150, + "height": 126, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "borderRadius": 0, + "fill": "#EDF0FD", + "stroke": "#0D32B2", + "shadow": false, + "3d": false, + "multiple": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "blend": false, + "fields": null, + "methods": null, + "columns": null, + "label": "a", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "#0A0F25", + "italic": false, + "bold": false, + "underline": false, + "labelWidth": 12, + "labelHeight": 26, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "zIndex": 0, + "level": 2 + }, + { + "id": "foo.b", + "type": "", + "pos": { + "x": 286, + "y": 122 + }, + "width": 150, + "height": 126, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "borderRadius": 0, + "fill": "#EDF0FD", + "stroke": "#0D32B2", + "shadow": false, + "3d": false, + "multiple": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "blend": false, + "fields": null, + "methods": null, + "columns": null, + "label": "b", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "#0A0F25", + "italic": false, + "bold": false, + "underline": false, + "labelWidth": 13, + "labelHeight": 26, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "zIndex": 0, + "level": 2 + }, + { + "id": "foobar.c", + "type": "", + "pos": { + "x": 36, + "y": 742 + }, + "width": 150, + "height": 126, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "borderRadius": 0, + "fill": "#EDF0FD", + "stroke": "#0D32B2", + "shadow": false, + "3d": false, + "multiple": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "blend": false, + "fields": null, + "methods": null, + "columns": null, + "label": "c", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "#0A0F25", + "italic": false, + "bold": false, + "underline": false, + "labelWidth": 12, + "labelHeight": 26, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "zIndex": 0, + "level": 2 + }, + { + "id": "foobar.d", + "type": "", + "pos": { + "x": 286, + "y": 742 + }, + "width": 150, + "height": 126, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "borderRadius": 0, + "fill": "#EDF0FD", + "stroke": "#0D32B2", + "shadow": false, + "3d": false, + "multiple": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "blend": false, + "fields": null, + "methods": null, + "columns": null, + "label": "d", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "#0A0F25", + "italic": false, + "bold": false, + "underline": false, + "labelWidth": 13, + "labelHeight": 26, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "zIndex": 0, + "level": 2 + } + ], + "connections": [ + { + "id": "foo.(a -> b)[0]", + "src": "foo.a", + "srcArrow": "none", + "srcLabel": "", + "dst": "foo.b", + "dstArrow": "triangle", + "dstLabel": "", + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "stroke": "#0D32B2", + "label": "", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "#676C7E", + "italic": true, + "bold": false, + "underline": false, + "labelWidth": 0, + "labelHeight": 0, + "labelPosition": "", + "labelPercentage": 0, + "route": [ + { + "x": 111, + "y": 378 + }, + { + "x": 361, + "y": 378 + } + ], + "animated": false, + "tooltip": "", + "icon": null, + "zIndex": 4 + }, + { + "id": "foobar.(c -> d)[0]", + "src": "foobar.c", + "srcArrow": "none", + "srcLabel": "", + "dst": "foobar.d", + "dstArrow": "triangle", + "dstLabel": "", + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "stroke": "#0D32B2", + "label": "", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "#676C7E", + "italic": true, + "bold": false, + "underline": false, + "labelWidth": 0, + "labelHeight": 0, + "labelPosition": "", + "labelPercentage": 0, + "route": [ + { + "x": 111, + "y": 998 + }, + { + "x": 361, + "y": 998 + } + ], + "animated": false, + "tooltip": "", + "icon": null, + "zIndex": 4 + }, + { + "id": "(foo -> foobar)[0]", + "src": "foo", + "srcArrow": "none", + "srcLabel": "", + "dst": "foobar", + "dstArrow": "triangle", + "dstLabel": "", + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "stroke": "#0D32B2", + "label": "", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "#676C7E", + "italic": true, + "bold": false, + "underline": false, + "labelWidth": 0, + "labelHeight": 0, + "labelPosition": "", + "labelPercentage": 0, + "route": [ + { + "x": 236, + "y": 532 + }, + { + "x": 236, + "y": 632 + } + ], + "animated": false, + "tooltip": "", + "icon": null, + "zIndex": 0 + }, + { + "id": "(foo.a -- )[0]", + "src": "foo.a", + "srcArrow": "none", + "srcLabel": "", + "dst": "a-lifeline-end-2251863791", + "dstArrow": "none", + "dstLabel": "", + "opacity": 1, + "strokeDash": 6, + "strokeWidth": 2, + "stroke": "#0D32B2", + "label": "", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "#676C7E", + "italic": true, + "bold": false, + "underline": false, + "labelWidth": 0, + "labelHeight": 0, + "labelPosition": "", + "labelPercentage": 0, + "route": [ + { + "x": 111, + "y": 248 + }, + { + "x": 111, + "y": 508 + } + ], + "animated": false, + "tooltip": "", + "icon": null, + "zIndex": 1 + }, + { + "id": "(foo.b -- )[0]", + "src": "foo.b", + "srcArrow": "none", + "srcLabel": "", + "dst": "b-lifeline-end-668380428", + "dstArrow": "none", + "dstLabel": "", + "opacity": 1, + "strokeDash": 6, + "strokeWidth": 2, + "stroke": "#0D32B2", + "label": "", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "#676C7E", + "italic": true, + "bold": false, + "underline": false, + "labelWidth": 0, + "labelHeight": 0, + "labelPosition": "", + "labelPercentage": 0, + "route": [ + { + "x": 361, + "y": 248 + }, + { + "x": 361, + "y": 508 + } + ], + "animated": false, + "tooltip": "", + "icon": null, + "zIndex": 1 + }, + { + "id": "(foobar.c -- )[0]", + "src": "foobar.c", + "srcArrow": "none", + "srcLabel": "", + "dst": "c-lifeline-end-955173837", + "dstArrow": "none", + "dstLabel": "", + "opacity": 1, + "strokeDash": 6, + "strokeWidth": 2, + "stroke": "#0D32B2", + "label": "", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "#676C7E", + "italic": true, + "bold": false, + "underline": false, + "labelWidth": 0, + "labelHeight": 0, + "labelPosition": "", + "labelPercentage": 0, + "route": [ + { + "x": 111, + "y": 868 + }, + { + "x": 111, + "y": 1128 + } + ], + "animated": false, + "tooltip": "", + "icon": null, + "zIndex": 1 + }, + { + "id": "(foobar.d -- )[0]", + "src": "foobar.d", + "srcArrow": "none", + "srcLabel": "", + "dst": "d-lifeline-end-2106864010", + "dstArrow": "none", + "dstLabel": "", + "opacity": 1, + "strokeDash": 6, + "strokeWidth": 2, + "stroke": "#0D32B2", + "label": "", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "#676C7E", + "italic": true, + "bold": false, + "underline": false, + "labelWidth": 0, + "labelHeight": 0, + "labelPosition": "", + "labelPercentage": 0, + "route": [ + { + "x": 361, + "y": 868 + }, + { + "x": 361, + "y": 1128 + } + ], + "animated": false, + "tooltip": "", + "icon": null, + "zIndex": 1 + } + ] +} diff --git a/e2etests/testdata/regression/sequence_diagram_name_crash/elk/sketch.exp.svg b/e2etests/testdata/regression/sequence_diagram_name_crash/elk/sketch.exp.svg new file mode 100644 index 000000000..724c43730 --- /dev/null +++ b/e2etests/testdata/regression/sequence_diagram_name_crash/elk/sketch.exp.svg @@ -0,0 +1,31 @@ + +foofoobarabcd + + + \ No newline at end of file diff --git a/go.mod b/go.mod index 74e905dbd..7a7910207 100644 --- a/go.mod +++ b/go.mod @@ -21,7 +21,7 @@ require ( golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 gonum.org/v1/plot v0.12.0 nhooyr.io/websocket v1.8.7 - oss.terrastruct.com/util-go v0.0.0-20221213112904-c09378905bfb + oss.terrastruct.com/util-go v0.0.0-20221218203331-558e3b4adc95 ) require ( diff --git a/go.sum b/go.sum index fb6710cf6..e77eb6d53 100644 --- a/go.sum +++ b/go.sum @@ -806,8 +806,8 @@ honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9 honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= 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/util-go v0.0.0-20221213112904-c09378905bfb h1:ybqEeDAI/VyaVgPd5kis5aq+IPhHI3dQmdSxFG6SuuY= -oss.terrastruct.com/util-go v0.0.0-20221213112904-c09378905bfb/go.mod h1:Fwy72FDIOOM4K8F96ScXkxHHppR1CPfUyo9+x9c1PBU= +oss.terrastruct.com/util-go v0.0.0-20221218203331-558e3b4adc95 h1:Ya/jgBNhNwZ30BsEcyjf2bTT8dtCdTM2WOSMEeSGB3E= +oss.terrastruct.com/util-go v0.0.0-20221218203331-558e3b4adc95/go.mod h1:Fwy72FDIOOM4K8F96ScXkxHHppR1CPfUyo9+x9c1PBU= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= rsc.io/pdf v0.1.1 h1:k1MczvYDUvJBe93bYd7wrZLLUEcLZAuF824/I4e5Xr4= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= diff --git a/install.sh b/install.sh index bbe0ca02e..026f423c9 100755 --- a/install.sh +++ b/install.sh @@ -278,7 +278,7 @@ header() { bigheader() { set -- "$(echo "$*" | sed "s/^/ * /")" - FGCOLOR=${FGCOLOR:-3} logp "/**************************************************************** + FGCOLOR=${FGCOLOR:-6} logp "/**************************************************************** $* ****************************************************************/" } diff --git a/main.go b/main.go index f37c40c0a..53ef5d201 100644 --- a/main.go +++ b/main.go @@ -243,6 +243,10 @@ func compile(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, pad, if err != nil { return svg, false, err } + } else { + if len(out) > 0 && out[len(out)-1] != '\n' { + out = append(out, '\n') + } } err = ms.WritePath(outputPath, out) diff --git a/make.sh b/make.sh index c05c8b678..60a938426 100755 --- a/make.sh +++ b/make.sh @@ -7,6 +7,6 @@ if [ ! -e "$(dirname "$0")/ci/sub/.git" ]; then fi . "$(dirname "$0")/ci/sub/lib.sh" PATH="$(cd -- "$(dirname "$0")" && pwd)/ci/sub/bin:$PATH" -cd "$(dirname "$0")" +cd -- "$(dirname "$0")" _make "$@"