This is an automated email from the ASF dual-hosted git repository.
morningman pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/doris.git
The following commit(s) were added to refs/heads/master by this push:
new 61311a6b35d [fix](thirdparty) fix four ways the thirdparty download
step fails or ships corruption (#66650)
61311a6b35d is described below
commit 61311a6b35d1e93e3474c46fa03d1467914929db
Author: Mingyu Chen (Rayner) <[email protected]>
AuthorDate: Tue Aug 11 19:55:27 2026 +0800
[fix](thirdparty) fix four ways the thirdparty download step fails or ships
corruption (#66650)
### What problem does this PR solve?
Related PR: #66358
Problem Summary:
`sh build-thirdparty.sh arrow_adbc` fails with
```
-- Installing:
.../thirdparty/installed/lib64/pkgconfig/adbc-driver-sqlite.pc
.../thirdparty/src/adbc_driver_flightsql does not exist.
```
`build-thirdparty.sh` passes the package names straight through to
`download-thirdparty.sh`, which narrows `TP_ARCHIVES` to exactly the
named entries.
`ARROW_ADBC_FLIGHTSQL` is a companion archive of `arrow_adbc` rather
than a package of
its own -- it has no build function, `build_arrow_adbc()` only copies
the prebuilt
driver out of it -- so its name never appears on a command line and the
narrowing drops
it. The wheel is then neither downloaded nor unpacked, and
`check_if_source_exist` kills
the build.
A full `build-thirdparty.sh` run is unaffected:
| invocation | result |
| --- | --- |
| `build-thirdparty.sh` (no package argument) | ok |
| `build-thirdparty.sh arrow_adbc` on a clean tree | **fails** |
| `build-thirdparty.sh arrow_adbc` twice in a row | first ok, **second
fails** |
| `build-thirdparty.sh --clean arrow_adbc` | **fails** |
The third row is the confusing one: `cleanup_package_source()` deletes
the unpacked
driver once `arrow_adbc` is built, and the narrowed download list never
restores it, so
the first run succeeding is exactly what makes the second one fail.
The first commit pulls the companion archive in at the narrowing site,
guarded on the
platforms that register it -- upstream publishes no prebuilt driver for
macOS or other
architectures, where `ARROW_ADBC_FLIGHTSQL_SOURCE` stays unset and the
archive is not
part of `TP_ARCHIVES` to begin with.
The second commit is unrelated to the failure above, but was noticed
while debugging it.
The thirdparty scripts carry a `#!/usr/bin/env bash` shebang, which is
ignored when they
are started as `sh build-thirdparty.sh`. On Debian and Ubuntu `/bin/sh`
is dash, which
parses none of the `[[ ]]`, arrays and here-strings these scripts are
built on -- and
does not stop at the first one, it runs a mangled version of the script.
They now
re-exec themselves under bash.
The third commit fixes a CI failure this PR ran into, which predates it
and hits every
thirdparty build. The `doris-thirdparty` source bundle the jobs unpack
currently ships a
0-byte `src/tsan_interface_atomic.h` (it was still intact on 2026-08-01,
run
30679875385), so every job now fails its md5 check and falls back to
fetching the header
from `gcc.gnu.org`. That host is not reachable from every runner: in run
31463316509 the
Linux and macOS jobs got it, macOS-arm64 did not and the build died with
`Failed to
download tsan_interface_atomic.h`. `download_func()` loops twice, but
when a corrupt file
is already on disk the first pass is spent detecting and removing it, so
an unreachable
host gets a single attempt. A fallback now points at the GitHub gcc
mirror.
The fourth commit fixes why that 0-byte header was ever published. The
job that builds
the bundle runs on macOS, and `download-thirdparty.sh` disabled every
checksum when GNU
`md5sum` was absent, which is the normal state there. Combined with
`wget -q URL -O file`
creating its output file before the transfer, a failed download left a
0-byte file that
the unverified retry then reported as `Archive ... already exist`, and
the script exited
0 with a corrupt archive on disk. It now falls back to BSD `md5`.
This is the widest of the four: every macOS consumer gains verification
it silently
lacked -- the macOS jobs here, `be-ut-mac.yml`, the `doris-thirdparty`
release jobs, and
anyone building Doris on a Mac, where none of the ~87 archives were
being checked.
Note that the already-published bundle is still corrupt. It is rebuilt
whenever `git diff`
reports changes under `thirdparty/`, so merging this PR is itself the
trigger -- and by
then the rebuild will run with the checksum fix in place.
The fifth commit gives `thirdparty/test/` a CI job. Two of the three
scripts there were
referenced by no workflow and carried no executable bit, so they had
never run; guarding
`download-thirdparty.sh` with tests that never run is part of why the
checksum gap stayed
invisible for as long as it did. They stub out the downloads, so the job
needs none of the
toolchain the build jobs spend an hour preparing and finishes in
seconds.
### Release note
None
### Check List (For Author)
- Test
- [x] Manual test (add detailed scripts or steps below)
<details>
<summary>Manual test details</summary>
Linux / x86_64, with both archives already in `thirdparty/src` so
nothing is downloaded.
**Targeted download, before and after**
```
# before (upstream master)
$ bash download-thirdparty.sh arrow_adbc
Download and build specified libs only: ARROW_ADBC
$ ls src/ | grep '^adbc_driver_flightsql$'
(nothing -- this is the state build_arrow_adbc() then dies on)
# after
$ bash download-thirdparty.sh arrow_adbc
Download and build specified libs only: ARROW_ADBC ARROW_ADBC_FLIGHTSQL
$ ls -la src/adbc_driver_flightsql/libadbc_driver_flightsql.so
-rwxr-xr-x 1 ... 33693080 ...
src/adbc_driver_flightsql/libadbc_driver_flightsql.so
```
**Full targeted build, run twice from a tree that only holds the
archives**
```
$ sh build-thirdparty.sh arrow_adbc # RUN1_EXIT=0
$ sh build-thirdparty.sh arrow_adbc # RUN2_EXIT=0
$ ls installed/lib64/ | grep adbc
libadbc_driver_flightsql.so
libadbc_driver_jni.so
libadbc_driver_manager.a
...
```
**Edge cases**
```
$ bash download-thirdparty.sh arrow_adbc_flightsql
Download and build specified libs only: ARROW_ADBC_FLIGHTSQL #
companion does not drag its owner in
$ bash download-thirdparty.sh arrow_adbc arrow_adbc_flightsql
Download and build specified libs only: ARROW_ADBC ARROW_ADBC_FLIGHTSQL #
not duplicated
# with `uname -s` stubbed to Darwin, so ARROW_ADBC_FLIGHTSQL_SOURCE stays
unset
$ bash download-thirdparty.sh arrow_adbc
Download and build specified libs only: ARROW_ADBC #
companion correctly not added
```
Removing the `-n "${ARROW_ADBC_FLIGHTSQL_SOURCE}"` guard makes that last
case fail with
`Failed to download ` (empty archive name), so the guard is load bearing
rather than
decorative.
**Second commit, under a real dash** (`python:3.11-slim`, `/bin/sh` ->
`/usr/bin/dash`)
```
# before
$ sh build-thirdparty.sh -h ; echo $?
build-thirdparty.sh: 34: Bad substitution
build-thirdparty.sh: 42: [[: not found
build-thirdparty.sh: 72: [[: not found
build-thirdparty.sh: 114: [[: not found
build-thirdparty.sh: 120: Syntax error: redirection unexpected
2
# after -- re-execs under bash and reaches the real toolchain check
# (the container has no LDB toolchain, which is why it stops there)
$ sh build-thirdparty.sh -h ; echo $?
Target system: Linux; Target arch: x86_64
Python 3.11.15
Error: wrong directory DORIS_GCC_HOME=./..
1
```
Under bash the guard is a no-op: `BASH_VERSION` is set, so no re-exec
happens and
behaviour is unchanged.
**Third commit, reproducing the exact CI failure locally**
A 0-byte `tsan_interface_atomic.h` on disk plus an unreachable primary
host, which is
what the macOS-arm64 runner saw:
```
$ printf '' > src/tsan_interface_atomic.h # same md5 as the
bundle's copy
$ bash download-thirdparty.sh tsan_header # TSAN_HEADER_DOWNLOAD
temporarily broken
src/tsan_interface_atomic.h md5sum check failed!
Archive tsan_interface_atomic.h will be removed and download again.
Downloading tsan_interface_atomic.h from
https://gcc.gnu.org.invalid/unreachable-on-purpose ...
Failed to download tsan_interface_atomic.h. attemp: 2
Failed to download tsan_interface_atomic.h
src/tsan_interface_atomic.h md5sum check failed!
Archive tsan_interface_atomic.h will be removed and download again.
Downloading tsan_interface_atomic.h from
https://raw.githubusercontent.com/gcc-mirror/gcc/...
Success to download tsan_interface_atomic.h
$ md5sum src/tsan_interface_atomic.h
d72679bea167d6a513d959f5abd149dc # matches
TSAN_HEADER_MD5SUM
```
**Fourth commit**
`thirdparty/test/download-thirdparty-md5-test.sh` is added alongside it.
It imitates macOS
with a PATH that holds no `md5sum` and a BSD `md5` stub, so it runs on
any host, and feeds
the script an empty body returned with a success status -- a case only a
checksum can
catch. All three scripts under `thirdparty/test/` pass:
```
[PASS rc=0] download-thirdparty-fallback-test.sh
[PASS rc=0] juicefs-default-mirror-test.sh
[PASS rc=0] download-thirdparty-md5-test.sh
```
The first two are the pre-existing ones, run to confirm that changing
the comparison in
`md5sum_func()` from whole output lines to bare digests did not break
them.
Dropping the `md5` fallback again makes the new test fail, so it is not
a test that cannot
fail:
```
FAIL: download-thirdparty.sh accepted an empty archive
```
---
.github/workflows/build-thirdparty.yml | 18 +++
thirdparty/build-thirdparty.sh | 11 ++
thirdparty/download-thirdparty.sh | 47 ++++++-
.../test/download-thirdparty-fallback-test.sh | 0
thirdparty/test/download-thirdparty-md5-test.sh | 149 +++++++++++++++++++++
thirdparty/test/juicefs-default-mirror-test.sh | 0
thirdparty/vars.sh | 5 +
7 files changed, 223 insertions(+), 7 deletions(-)
diff --git a/.github/workflows/build-thirdparty.yml
b/.github/workflows/build-thirdparty.yml
index 5e04a7f9668..d5a6fe2b2bd 100644
--- a/.github/workflows/build-thirdparty.yml
+++ b/.github/workflows/build-thirdparty.yml
@@ -80,6 +80,24 @@ jobs:
- name: Test focused thirdparty lifecycle
run: thirdparty/test/arrow-paimon-lifecycle-test.sh
+ download_script_test:
+ name: Download Script Test
+ needs: changes
+ if: ${{ needs.changes.outputs.thirdparty_changes == 'true' }}
+ runs-on: ubuntu-22.04
+ steps:
+ - name: Checkout ${{ github.ref }}
+ uses: actions/checkout@v4
+
+ # These drive download-thirdparty.sh against stubbed downloads, so they
+ # need none of the toolchain the build jobs spend an hour preparing and
+ # finish in seconds. They were sitting in the tree unreferenced until
now.
+ - name: Test download-thirdparty.sh
+ run: |
+ thirdparty/test/download-thirdparty-fallback-test.sh
+ thirdparty/test/juicefs-default-mirror-test.sh
+ thirdparty/test/download-thirdparty-md5-test.sh
+
build_linux:
name: Build Third Party Libraries (Linux)
needs: changes
diff --git a/thirdparty/build-thirdparty.sh b/thirdparty/build-thirdparty.sh
index a6544a57ac2..f44122ca1f3 100755
--- a/thirdparty/build-thirdparty.sh
+++ b/thirdparty/build-thirdparty.sh
@@ -29,6 +29,17 @@
# to check if all thirdparties have been downloaded, unpacked and patched.
#################################################################################
+# The shebang above only takes effect when this script is executed directly.
+# `sh build-thirdparty.sh` hands it to /bin/sh instead, which is dash on Debian
+# and Ubuntu and parses none of the `[[ ]]`, arrays and here-strings this
script
+# is built on. It does not stop at the first of them either, it keeps going and
+# runs a mangled version of the script. Re-exec under bash so that the way the
+# script was invoked cannot decide whether the build works. Keep this block
+# POSIX, it has to be parsed by the shell that is about to be replaced.
+if [ -z "${BASH_VERSION:-}" ]; then
+ exec bash "$0" "$@"
+fi
+
set -eo pipefail
curdir="$(cd "$(dirname "${BASH_SOURCE[0]}")" &>/dev/null && pwd)"
diff --git a/thirdparty/download-thirdparty.sh
b/thirdparty/download-thirdparty.sh
index ea9436be2de..7c967d31dd9 100755
--- a/thirdparty/download-thirdparty.sh
+++ b/thirdparty/download-thirdparty.sh
@@ -23,6 +23,17 @@
# Things will only be downloaded, unpacked and patched once.
################################################################
+# The shebang above only takes effect when this script is executed directly.
+# `sh download-thirdparty.sh` hands it to /bin/sh instead, which is dash on
+# Debian and Ubuntu and parses none of the `[[ ]]`, arrays and here-strings
this
+# script is built on. It does not stop at the first of them either, it keeps
+# going and runs a mangled version of the script. Re-exec under bash so that
the
+# way the script was invoked cannot decide whether the download works. Keep
this
+# block POSIX, it has to be parsed by the shell that is about to be replaced.
+if [ -z "${BASH_VERSION:-}" ]; then
+ exec bash "$0" "$@"
+fi
+
set -eo pipefail
curdir="$(cd "$(dirname "${BASH_SOURCE[0]}")" &>/dev/null && pwd)"
@@ -74,13 +85,30 @@ while [[ $# -gt 0 ]]; do
)
done
if [[ "${SPEC_LIB}" != "" ]]; then
+ # ARROW_ADBC_FLIGHTSQL is a companion archive of arrow_adbc rather than a
+ # package of its own: it has no build function, build_arrow_adbc() only
copies
+ # the prebuilt driver out of it. Its name therefore never appears on a
command
+ # line, so narrowing to the named entries alone leaves it unfetched and
+ # `build-thirdparty.sh arrow_adbc` dies on the missing source directory.
+ if [[ -n "${ARROW_ADBC_FLIGHTSQL_SOURCE}" ]] &&
+ [[ " ${SPEC_ARCHIVES[*]} " == *' ARROW_ADBC '* ]] &&
+ [[ " ${SPEC_ARCHIVES[*]} " != *' ARROW_ADBC_FLIGHTSQL '* ]]; then
+ SPEC_ARCHIVES+=('ARROW_ADBC_FLIGHTSQL')
+ fi
TP_ARCHIVES=("${SPEC_ARCHIVES[@]}")
echo "Download and build specified libs only: ${TP_ARCHIVES[*]}"
fi
md5sum_bin='md5sum'
if ! command -v "${md5sum_bin}" >/dev/null 2>&1; then
- echo "Warn: md5sum is not installed"
+ # macOS ships BSD md5 rather than GNU md5sum. Giving up on verification
there
+ # is not a neutral loss: `wget -O` creates its output file before the
transfer,
+ # so a failed download leaves a 0-byte file behind, and an unverified retry
+ # then reports it as a valid cached archive and carries on.
+ md5sum_bin='md5'
+fi
+if ! command -v "${md5sum_bin}" >/dev/null 2>&1; then
+ echo "Warn: neither md5sum nor md5 is installed, archives will not be
verified"
md5sum_bin=""
fi
@@ -92,13 +120,18 @@ md5sum_func() {
if [[ "${md5sum_bin}" == "" ]]; then
return 0
+ fi
+
+ # Compare the digest alone, the two tools disagree on everything around it.
+ if [[ "${md5sum_bin}" == 'md5' ]]; then
+ md5="$("${md5sum_bin}" -q "${DESC_DIR}/${FILENAME}")"
else
- md5="$(md5sum "${DESC_DIR}/${FILENAME}")"
- if [[ "${md5}" != "${MD5SUM} ${DESC_DIR}/${FILENAME}" ]]; then
- echo "${DESC_DIR}/${FILENAME} md5sum check failed!"
- echo -e "except-md5 ${MD5SUM} \nactual-md5 ${md5}"
- return 1
- fi
+ md5="$("${md5sum_bin}" "${DESC_DIR}/${FILENAME}" | awk '{ print $1 }')"
+ fi
+ if [[ "${md5}" != "${MD5SUM}" ]]; then
+ echo "${DESC_DIR}/${FILENAME} md5sum check failed!"
+ echo -e "except-md5 ${MD5SUM} \nactual-md5 ${md5}"
+ return 1
fi
return 0
}
diff --git a/thirdparty/test/download-thirdparty-fallback-test.sh
b/thirdparty/test/download-thirdparty-fallback-test.sh
old mode 100644
new mode 100755
diff --git a/thirdparty/test/download-thirdparty-md5-test.sh
b/thirdparty/test/download-thirdparty-md5-test.sh
new file mode 100755
index 00000000000..143fc484dd9
--- /dev/null
+++ b/thirdparty/test/download-thirdparty-md5-test.sh
@@ -0,0 +1,149 @@
+#!/usr/bin/env bash
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+# download-thirdparty.sh used to disable every checksum when GNU md5sum was
+# absent, which is the normal state on macOS. That is not a neutral loss of
+# coverage: an empty or truncated response is then indistinguishable from a
+# valid archive, and gets treated as one. A 0-byte header reached the published
+# thirdparty source bundle exactly that way, because the job that builds it
runs
+# on macOS. This test pins the invariant that a bad download is rejected on a
+# host that only has BSD md5.
+
+set -eo pipefail
+
+ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." &>/dev/null && pwd)"
+
+fail() {
+ echo "FAIL: $*" >&2
+ exit 1
+}
+
+assert_eq() {
+ local expected="$1"
+ local actual="$2"
+ [[ "${actual}" == "${expected}" ]] || fail "expected '${expected}', got
'${actual}'"
+}
+
+command -v md5sum >/dev/null 2>&1 || fail "this test needs md5sum on the host
to build its fixtures"
+
+tmpdir="$(mktemp -d)"
+trap 'rm -rf "${tmpdir}"' EXIT
+
+payload='thirdparty archive payload'
+payload_md5="$(printf '%s' "${payload}" | md5sum | awk '{ print $1 }')"
+
+# download-thirdparty.sh resolves its helpers through PATH, so imitating macOS
+# only takes a PATH that holds everything the script needs except md5sum. That
+# runs on any host, which is the point -- the platform this protects is the one
+# CI has the least of.
+sandbox_bin="${tmpdir}/bin"
+mkdir -p "${sandbox_bin}"
+for tool in bash awk sed tr rm mkdir dirname basename uname cat cut head ls
find sort tar unzip git curl env readlink; do
+ tool_path="$(command -v "${tool}" 2>/dev/null)" || continue
+ ln -sf "${tool_path}" "${sandbox_bin}/${tool}"
+done
+
+# BSD md5 as macOS ships it: `md5 -q FILE` prints the digest and nothing else.
+cat >"${sandbox_bin}/md5" <<EOF
+#!/usr/bin/env bash
+set -eo pipefail
+[[ "\$1" == '-q' ]] || { echo 'stub md5 only implements -q' >&2; exit 64; }
+$(command -v md5sum) "\$2" | awk '{ print \$1 }'
+EOF
+chmod +x "${sandbox_bin}/md5"
+
+PATH="${sandbox_bin}" command -v md5sum >/dev/null 2>&1 &&
+ fail "sandbox PATH still exposes md5sum, the test would not prove anything"
+
+cat >"${tmpdir}/vars.sh" <<EOF
+#!/bin/bash
+export TP_SOURCE_DIR="${tmpdir}/src"
+export TP_INSTALL_DIR="${tmpdir}/installed"
+export TP_PATCH_DIR="${tmpdir}/patches"
+export TP_INCLUDE_DIR="\${TP_INSTALL_DIR}/include"
+export TP_LIB_DIR="\${TP_INSTALL_DIR}/lib"
+export TP_JAR_DIR="\${TP_INSTALL_DIR}/lib/jar"
+FOO_DOWNLOAD="https://example.com/foo.jar"
+FOO_NAME="foo.jar"
+FOO_SOURCE=
+FOO_MD5SUM="${payload_md5}"
+export TP_ARCHIVES=('FOO')
+EOF
+
+# A wget that reports success and writes nothing, which is what an empty 200
+# response looks like. Only a checksum can catch this one, so it fails the run
+# if and only if verification really happened.
+write_wget() {
+ local body="$1"
+ cat >"${sandbox_bin}/wget" <<EOF
+#!/usr/bin/env bash
+set -eo pipefail
+output=''
+expect_output=0
+for arg in "\$@"; do
+ if [[ "\${expect_output}" -eq 1 ]]; then
+ output="\${arg}"
+ expect_output=0
+ continue
+ fi
+ [[ "\${arg}" == '-O' ]] && expect_output=1
+done
+printf '%s' '${body}' >"\${output}"
+exit 0
+EOF
+ chmod +x "${sandbox_bin}/wget"
+}
+
+run_download() {
+ PATH="${sandbox_bin}" TP_DIR="${tmpdir}" DORIS_HOME="${ROOT}/.." \
+ bash "${ROOT}/download-thirdparty.sh" 2>&1
+}
+
+echo '== an empty response must not pass as a valid archive =='
+rm -rf "${tmpdir}/src"
+write_wget ''
+if output="$(run_download)"; then
+ echo "${output}"
+ fail 'download-thirdparty.sh accepted an empty archive'
+fi
+case "${output}" in
+*'md5sum check failed'*) ;;
+*)
+ echo "${output}"
+ fail 'expected the empty archive to be rejected by its checksum'
+ ;;
+esac
+case "${output}" in
+*'will not be verified'*)
+ echo "${output}"
+ fail 'verification was skipped, BSD md5 was not picked up'
+ ;;
+esac
+[[ -f "${tmpdir}/src/foo.jar" ]] && fail 'the rejected archive was left behind
for the next run to reuse'
+
+echo '== a good archive must still be accepted through BSD md5 =='
+rm -rf "${tmpdir}/src"
+write_wget "${payload}"
+if ! output="$(run_download)"; then
+ echo "${output}"
+ fail 'download-thirdparty.sh rejected a valid archive'
+fi
+[[ -f "${tmpdir}/src/foo.jar" ]] || fail "expected downloaded archive at
${tmpdir}/src/foo.jar"
+assert_eq "${payload}" "$(cat "${tmpdir}/src/foo.jar")"
+
+echo 'PASS'
diff --git a/thirdparty/test/juicefs-default-mirror-test.sh
b/thirdparty/test/juicefs-default-mirror-test.sh
old mode 100644
new mode 100755
diff --git a/thirdparty/vars.sh b/thirdparty/vars.sh
index f8eedabe0fc..ae300077828 100644
--- a/thirdparty/vars.sh
+++ b/thirdparty/vars.sh
@@ -386,6 +386,11 @@ AWS_SDK_MD5SUM="80aa616efe1a3e7a9bf0dfbc44a97864"
# tsan_header
TSAN_HEADER_DOWNLOAD="https://gcc.gnu.org/git/?p=gcc.git;a=blob_plain;f=libsanitizer/include/sanitizer/tsan_interface_atomic.h;hb=refs/heads/releases/gcc-7"
+# Not every CI runner can reach gcc.gnu.org, and download_func() spends the
first
+# of its two passes re-checking the file that is already on disk, so an
+# unreachable host costs a single attempt and then fails the build. The gcc-7
+# branch is closed, so this mirror serves a byte identical file.
+TSAN_HEADER_FALLBACK_DOWNLOAD="https://raw.githubusercontent.com/gcc-mirror/gcc/releases/gcc-7/libsanitizer/include/sanitizer/tsan_interface_atomic.h"
TSAN_HEADER_NAME="tsan_interface_atomic.h"
TSAN_HEADER_FILE="tsan_interface_atomic.h"
TSAN_HEADER_MD5SUM="d72679bea167d6a513d959f5abd149dc"
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]