Skip to content

Instantly share code, notes, and snippets.

@Lohann
Last active December 18, 2025 18:53
Show Gist options
  • Select an option

  • Save Lohann/0dd35a31127698ba64708ba57741501d to your computer and use it in GitHub Desktop.

Select an option

Save Lohann/0dd35a31127698ba64708ba57741501d to your computer and use it in GitHub Desktop.
#!/usr/bin/env bash
set -euo pipefail
# Prevent locale nonsense from breaking basic text processing.
LC_ALL=C
export LC_ALL
# display a message then exit
abort() {
printf "%s\n" "$@" >&2
exit 1
}
# Fail fast with a concise message when not using bash
if [ -z "${BASH_VERSION:-}" ]
then
abort "Bash is required to interpret this script."
fi
# check required binaries
require_bin(){
[[ "$#" -eq '1' ]] || abort "[BUG]: usage \"require_bin <bin_name>\""
[[ -z "${1}" ]] && abort "[BUG]: bin_name cannot be empty"
command -v "${1}" > /dev/null 2>&1 || abort "'${1}' not found"
}
# check dependencies
require_bin dirname
require_bin basename
require_bin uname
require_bin pkill
require_bin mkdir
require_bin make
require_bin sed
require_bin pkg-config
require_bin chmod
require_bin sha256sum
require_bin cut
require_bin tar
require_bin patch
# make sure we are in the right directory
cd -- "$(dirname "${0}")" || exit 1
################################
# Detect Host Operating System #
################################
detect_os(){
local os_name
# Detect OS using `uname -s`
if command -v uname > /dev/null 2>&1; then
os_name="$(uname -s)"
case "${os_name}" in
'Darwin') os_name='darwin' ;;
'Linux') os_name='linux' ;;
'FreeBSD') os_name='freebsd' ;;
'OpenBSD') os_name='openbsd' ;;
'NetBSD') os_name='netbsd' ;;
'DragonFlyBSD') os_name='dragonflybsd' ;;
'SunOS') os_name='solaris' ;;
'AIX') os_name='aix' ;;
*) echo "Unknown OS: ${os_name}"; return 1 ;;
esac
echo "${os_name}"
return 0
fi
# Detect OS using $OSTYPE
if [[ -n "${OSTYPE++}" ]]; then
case "${OSTYPE}" in
'linux-gnu'*) os_name='linux' ;;
'freebsd'*) os_name='freebsd' ;;
'darwin'*) os_name='darwin' ;;
'cygwin') os_name='windows' ;;
'win32') os_name='windows' ;;
'msys') os_name='windows' ;; # Git Bash
*) echo "Unknown OS: ${OSTYPE}"; return 1 ;;
esac;
echo "${os_name}"
return 0
fi
return 1
}
############################
# Detect Host Architecture #
############################
detect_arch(){
local os_arch
# Detect architecture using `dpkg --print-architecture`
if command -v dpkg > /dev/null 2>&1; then
os_arch="$(dpkg --print-architecture)"
case "${os_arch}" in
'amd64') os_arch='x86_64' ;;
'i386') os_arch='x86' ;;
'armhf') os_arch='arm' ;;
'arm64') os_arch='aarch64' ;;
'ppc64el') os_arch='powerpc64le' ;;
's390x') os_arch='s390x' ;;
'riscv64') os_arch='riscv64' ;;
*) echo "Unsupported architecture: ${os_arch}"; return 1 ;;
esac
echo "${os_arch}"
return 0
fi
# Detect architecture using `uname -m`
if command -v uname > /dev/null 2>&1; then
os_arch="$(uname -m)"
case "${os_arch}" in
x86_64) os_arch='x86_64' ;;
i*86) os_arch='x86' ;;
aarch64|arm64) os_arch='aarch64' ;;
armv6l|armv7l) os_arch='arm' ;;
riscv64) os_arch='riscv64' ;;
ppc64le|rs6000) os_arch='powerpc64le' ;;
*) echo "Unsupported architecture: ${os_arch}"; return 1 ;;
esac
echo "${os_arch}"
return 0
fi
return 1
}
###################
# Download Fossil #
###################
download_and_extract_fossil(){
# usage: download_and_extract_fossil
# obs: fossil will be download in the current directory.
local os_name
local os_arch
local fossil_url
local fossil_sha256
os_name="$(detect_os)"
os_arch="$(detect_arch)"
# Find compatible fossil binary
case "${os_name}-${os_arch}" in
'linux-x86_64')
fossil_url='https://fossil-scm.org/home/uv/fossil-linux-x64-2.27.tar.gz'
fossil_sha256='9bc289130b40a8fa1c45ea7966238b22ccdbb367495490cc43715a82de0cba0c'
;;
'linux-arm')
fossil_url='https://fossil-scm.org/home/uv/fossil-pi-2.27.tar.gz'
fossil_sha256='e91da0abb7a06a190ed55f980da19a487340e529adb036995702f69be2fb4aba'
;;
'darwin-x86_64')
fossil_url='https://fossil-scm.org/home/uv/fossil-mac-x64-2.27.tar.gz'
fossil_sha256='06123be3d292a3b3d6177a02fca78d0ec34f8d27f5fa35151053ce393b8744c0'
;;
'darwin-aarch64')
fossil_url='https://fossil-scm.org/home/uv/fossil-mac-arm-2.27.tar.gz'
fossil_sha256='c10e15af056c11ba76f11b8d120fe51d58e3cc27944bece810005c179863ab4f'
;;
*)
echo "Unsupported system: ${os_name}-${os_arch}"
return 1
;;
esac;
# Download fossil
if [[ -f 'fossil-2.27.tar.gz' ]]; then
if [[ "$(sha256sum ./fossil-2.27.tar.gz)" != "${fossil_sha256} ./fossil-2.27.tar.gz" ]]; then
rm -fv ./fossil-2.27.tar.gz
wget -O 'fossil-2.27.tar.gz' "${fossil_url}"
fi
else
wget -O 'fossil-2.27.tar.gz' "${fossil_url}"
fi
# Check checksum and extract
echo "${fossil_sha256} ./fossil-2.27.tar.gz" | sha256sum --check -
tar --extract --file ./fossil-2.27.tar.gz
rm ./fossil-2.27.tar.gz
return 0
}
absolute_path(){
if [[ "$#" -lt '1' ]]; then return 1; fi
pushd "$1" &> /dev/null
if [[ $? != 0 ]]; then return 1; fi
pwd -P
PWD_EXIT_CODE="$?"
popd &> /dev/null
return "${PWD_EXIT_CODE}"
}
########################
# Clone SQLITE3 Source #
########################
clone_sqlite3_source(){
# usage: clone_sqlite3_source <bin-dir>
# obs: <bin-dir> is where the fossil binary will be stored.
local fossil_bin
local bin_dir
[[ "$#" -eq '1' ]] || { echo >&2 "[BUG]: usage \"clone_sqlite3_source <bin-dir>\""; exit 1; }
bin_dir="${1}"
[[ -d "${bin_dir}" ]] || { echo >&2 "[BUG]: bin directory not found: \"${bin_dir}\""; exit 1; }
if [[ -x "${bin_dir}/fossil" ]]; then
fossil_bin="${bin_dir}/fossil"
elif command -v fossil > /dev/null 2>&1; then
fossil_bin="$(command -v fossil)"
else
# Check if `wget` and `sha256sum` exists
require_bin wget
require_bin sha256sum
# Download fossil
download_and_extract_fossil
fossil_bin="$(pwd)/fossil"
# Move to bin_dir
if [[ "${fossil_bin}" != "${bin_dir}/fossil" ]]; then
mv "${fossil_bin}" "${bin_dir}/fossil"
fossil_bin="${bin_dir}/fossil"
fi
fi
# Check fossil
test -f "${fossil_bin}" > /dev/null 2>&1 || { echo >&2 "'${fossil_bin}' not found"; exit 1; }
test -x "${fossil_bin}" > /dev/null 2>&1 || { echo >&2 "'${fossil_bin}' not executable"; exit 1; }
"${fossil_bin}" version > /dev/null 2>&1
# Checkout sqlite source
"${fossil_bin}" open https://sqlite.org/src
"${fossil_bin}" update version-3.51.1
}
#################
# Build SQLITE3 #
#################
build_sqlite3(){
# usage: build_sqlite3 <prefix> <sqlite-source>
# obs: current $PWD is the build directory
local prefix_dir
local sqlite_dir
[[ "$#" -eq '2' ]] || abort "[BUG]: usage \"build_sqlite3 <prefix> <sqlite-src>\""
prefix_dir="${1}"
sqlite_dir="${2}"
[[ -d "${sqlite_dir}" ]] || abort "sqlite source not found: '${sqlite_dir}'"
[[ -d "${prefix_dir}" ]] || abort "prefix not found: '${workspace_dir}'"
[[ -d "${prefix_dir}/lib" ]] || abort "<prefix>/lib not found"
[[ -d "${prefix_dir}/bin" ]] || abort "<prefix>/bin not found"
[[ -d "${prefix_dir}/share" ]] || abort "<prefix>/share not found"
local build_dir="$(pwd)"
[[ -d "${build_dir}" ]] || abort "build directory not found: '${build_dir}'"
[[ "${prefix_dir}" == "${build_dir}" ]] && abort "<prefix> must be different from <build_dir>"
[[ "${sqlite_dir}" == "${build_dir}" ]] && abort "<sqlite_src> must be different from <build_dir>"
local sqlite_config
sqlite_config=(
'-DSQLITE_ENABLE_API_ARMOR=1'
'-DSQLITE_ENABLE_COLUMN_METADATA=1'
'-DSQLITE_ENABLE_DBSTAT_VTAB=1'
'-DSQLITE_ENABLE_FTS3=1'
'-DSQLITE_ENABLE_FTS3_PARENTHESIS=1'
'-DSQLITE_ENABLE_FTS5=1'
'-DSQLITE_ENABLE_GEOPOLY=1'
'-DSQLITE_ENABLE_JSON1=1'
'-DSQLITE_ENABLE_MEMORY_MANAGEMENT=1'
'-DSQLITE_ENABLE_RTREE=1'
'-DSQLITE_ENABLE_STAT4=1'
'-DSQLITE_ENABLE_UNLOCK_NOTIFY=1'
'-DSQLITE_MAX_VARIABLE_NUMBER=250000'
'-DSQLITE_USE_URI=1'
'-DSQLITE_THREADSAFE=1'
'-DSQLITE_STRICT_SUBTYPE=1'
'-DSQLITE_ENABLE_MATH_FUNCTIONS=1'
'-DSQLITE_ENABLE_LOAD_EXTENSION=1'
'-DSQLITE_ENABLE_UPDATE_DELETE_LIMIT=1'
'-DSQLITE_ENABLE_NORMALIZE=1'
'-DSQLITE_ENABLE_DBSTAT_VTAB=1'
'-DSQLITE_ENABLE_DBPAGE_VTAB=1'
'-DSQLITE_ENABLE_BYTECODE_VTAB=1'
'-DSQLITE_ENABLE_PREUPDATE_HOOK=1'
'-DSQLITE_ENABLE_SESSION=1'
'-DSQLITE_ENABLE_SNAPSHOT=1'
'-DSQLITE_ENABLE_UNKNOWN_SQL_FUNCTION=1'
'-DSQLITE_ENABLE_STMTVTAB=1'
# CONFIG
'-DSQLITE_JSON_MAX_DEPTH=10'
# OMIT
'-DSQLITE_OMIT_DEPRECATED=1'
'-DSQLITE_OMIT_UTF16=1'
'-DSQLITE_OMIT_TCL_VARIABLE=1'
'-DSQLITE_OMIT_AUTHORIZATION=1'
# '-DSQLITE_OMIT_FLOATING_POINT=1'
)
sqlite_config="${sqlite_config[*]}"
printf "\nSQLITE OPTIONS: %s\n\n" "${sqlite_config}"
export CPPFLAGS="${sqlite_config}"
# Setup *.c compiler
if [[ -z "${CC++}" ]]; then
if command -v clang > /dev/null 2>&1; then
export CC="$(command -v clang)"
elif command -v gcc > /dev/null 2>&1; then
export CC="$(command -v gcc)"
elif command -v cc > /dev/null 2>&1; then
export CC="$(command -v cc)"
fi
fi
# Setup *.cpp compiler
if [[ -z "${CXX++}" ]]; then
if command -v clang++ > /dev/null 2>&1; then
export CXX="$(command -v clang++)"
elif command -v gcc > /dev/null 2>&1; then
export CXX="$(command -v gcc)"
fi
fi
printf '\n ------------------------------ configure ------------------------------ \n'
# Configure SQLITE3
pkg-config --exists --print-errors readline
local readline_cflags
local readline_ldflags
readline_cflags="$(pkg-config --static --cflags readline)"
readline_ldflags="$(pkg-config --static --libs readline)"
# Configure SQLITE3
"${sqlite_dir}/configure" \
--prefix="${workspace_dir}" \
--mandir="${workspace_dir}/share/man" \
--bindir="${workspace_dir}/bin" \
--libdir="${workspace_dir}/lib" \
--sbindir="${workspace_dir}/sbin" \
--sysconfdir="${workspace_dir}/etc" \
--includedir="${workspace_dir}/include" \
--localstatedir="${workspace_dir}/var" \
--enable-readline \
--disable-editline \
--enable-threadsafe \
--session \
--with-readline-cflags="${readline_cflags}" \
--with-readline-ldflags="${readline_ldflags}" \
--disable-tcl \
--disable-amalgamation \
--disable-shared
printf '\n ------------------------------ make lib ------------------------------- \n'
# build sqlite3
make lib
printf '\n ---------------------------- make install ----------------------------- \n'
# install
make install
}
###################
# Extract Tarball #
###################
unpack_tarball(){
# usage: unpack_tarball <tarball-file> <dest-dir>
local filename
local buildir
local source_path
local source_dir
local dest_path
if [ "$#" -ne 2 ]; then
echo "expected 3 parameters, got $# : unpack_tarball <filename> <buildir>" >&2 # write to stderr
exit 1
fi
filename="${1}"
source_dir="$(dirname "${filename}")"
filename="$(basename "${filename}")"
dest_path="${2}"
buildir="$(basename "${dest_path}")"
source_path="${source_dir}/${filename}"
printf " ------ ${buildir} ------\n"
if [[ "${source_path}" == "${dest_path}" ]]; then
echo "source_path and dest_path cannot be the same: ${source_path}"
exit 1
fi
if [[ ! -f "${source_path}" ]]; then
echo "source file not found: ${source_path}"
exit 1
fi
if [ -f "${dest_path}" ]; then
echo "destination directory is a file: ${dest_path}"
exit 1
elif [ -d "${dest_path}" ]; then
rm -rvf "${dest_path}"
fi
pushd "$(dirname "${dest_path}")" > /dev/null 2>&1
tar --extract --file "${source_path}"
popd > /dev/null 2>&1
if [ ! -e "${dest_path}" ]; then
printf "Extracted %s but artifact found: %s\n" "${filename}" "${dest_path}"
exit 1
fi
}
###############################
# download and check checksum #
###############################
download_package(){
# usage: download_package <url> [<filename>] <checksum>
local url
local filename
local filepath
local checksum
local actual_checksum
local sources_dir
sources_dir="$(pwd)"
if [[ ! -d "${sources_dir}" ]]; then
echo "download directory doesn't exists: ${sources_dir}" >&2 # write to stderr
exit 1
fi
if [[ "$#" -eq 3 ]]; then
filename="${2}"
checksum="${3}"
elif [[ "$#" -eq 2 ]]; then
filename="$(basename ${1})"
checksum="${2}"
else
echo "expected 2-3 parameters, got $# : download_package <url> [<filename>] <checksum>" >&2 # write to stderr
exit 1
fi
url="${1}"
filepath="${sources_dir}/${filename}"
if ! [[ "${filename}" =~ ^[0-9a-zA-Z]+([._-][0-9a-zA-Z]+)+$ ]]; then
echo "invalid filename: ${filename}" >&2 # write to stderr
exit 1
fi
if [[ ! -f "${filepath}" ]]; then
wget "${url}" -O "${filepath}"
chmod 755 "${filepath}"
fi
if [[ ! -f "${filepath}" ]]; then
printf 'download failed: %s\n' "${url}"
exit 1
fi
actual_checksum="$(sha256sum "${filepath}" | cut -d' ' -f1)"
if [[ "${checksum}" != "${actual_checksum}" ]]; then
printf "checksum mismatch: %s\nexpect: %s\n got: %s\n" "${filename}" "${checksum}" "${actual_checksum}" >&2 # write to stderr
exit 1
fi
}
##########################
# compile readline 8.3.3 #
##########################
compile_readline(){
# usage: compile_readline
# Download readline source code and patches
printf 'downloading readline dependencies...\n'
pushd "${sources_dir}" > /dev/null 2>&1
download_package \
'https://ftp.gnu.org/gnu/readline/readline-8.3.tar.gz' \
'readline-8.3.tar.gz' \
'fe5383204467828cd495ee8d1d3c037a7eba1389c22bc6a041f627976f9061cc'
download_package \
'https://ftp.gnu.org/gnu/readline/readline-8.3-patches/readline83-001' \
'readline-8.3-001.patch' \
'21f0a03106dbe697337cd25c70eb0edbaa2bdb6d595b45f83285cdd35bac84de'
download_package \
'https://ftp.gnu.org/gnu/readline/readline-8.3-patches/readline83-002' \
'readline-8.3-002.patch' \
'e27364396ba9f6debf7cbaaf1a669e2b2854241ae07f7eca74ca8a8ba0c97472'
download_package \
'https://ftp.gnu.org/gnu/readline/readline-8.3-patches/readline83-003' \
'readline-8.3-003.patch' \
'72dee13601ce38f6746eb15239999a7c56f8e1ff5eb1ec8153a1f213e4acdb29'
popd > /dev/null 2>&1
# Unpack readline-8.3.tar.gz
unpack_tarball "${sources_dir}/readline-8.3.tar.gz" "${sources_dir}/readline-8.3"
# Apply patches
pushd "${sources_dir}/readline-8.3" > /dev/null 2>&1
# Reference: https://linuxfromscratch.org/lfs/view/12.4/chapter08/readline.html
patch -Np0 -i "${sources_dir}/readline-8.3-001.patch"
patch -Np0 -i "${sources_dir}/readline-8.3-002.patch"
patch -Np0 -i "${sources_dir}/readline-8.3-003.patch"
# Build readline
mkdir build
pushd 'build' > /dev/null 2>&1
../configure \
--srcdir="${sources_dir}/readline-8.3" \
--prefix="${workspace_dir}" \
--bindir="${workspace_dir}/bin" \
--libdir="${workspace_dir}/lib" \
--sbindir="${workspace_dir}/sbin" \
--sysconfdir="${workspace_dir}/etc" \
--includedir="${workspace_dir}/include" \
--localstatedir="${workspace_dir}/var" \
--enable-static \
--disable-shared \
--disable-install-examples \
--with-curses \
--without-shared-termcap-library \
--disable-doc \
--disable-docs \
--disable-DOCDIR
make -j1
make install
popd > /dev/null 2>&1
popd > /dev/null 2>&1
}
####################
# START THE SCRIPT #
####################
# Setup directories
root_dir="$(pwd)"
workspace_dir="${root_dir}/workspace"
sources_dir="${root_dir}/sources"
sqlite_dir="${sources_dir}/sqlite-v3.51.1"
build_dir="${root_dir}/build"
# Create sources directory
[[ -d "${sources_dir}" ]] || mkdir -pv "${sources_dir}"
# Cleanup workspace
[[ -d "${workspace_dir}" ]] && rm -rfv "${workspace_dir}"/*
mkdir -pv "${workspace_dir}"/{build,bin,lib,share/man}
rm -rf "${build_dir}"/*
# Download sqlite source
if ! test -f "${sqlite_dir}/configure"; then
mkdir -pv "${sqlite_dir}"
pushd "${sqlite_dir}" > /dev/null 2>&1
clone_sqlite3_source "${workspace_dir}/bin"
popd > /dev/null 2>&1
fi
# Check workspace and source directories
[[ -d "${sqlite_dir}" ]] || abort "sqlite source not found: '${sqlite_dir}'"
[[ -d "${workspace_dir}" ]] || abort "workspace directory not found: '${workspace_dir}'"
# Cleanup build directory
[[ -d "${build_dir}" ]] && rm -rf "${build_dir}"
mkdir -p "${build_dir}"
# Prepare C compiler environment
export PATH="${workspace_dir}/bin:${PATH}"
export LD_LIBRARY_PATH="${workspace_dir}/lib"
export PKG_CONFIG_PATH="${workspace_dir}/lib/pkgconfig"
##################
# readline 8.3.3 #
##################
printf '\n ---------------------------- readline 8.3.3 --------------------------- \n'
compile_readline
# Test if the readline pkg exists
pkg-config --exists --print-errors readline
printf '\n ----------------------------------------------------------------------- \n\n'
###########
# SQLITE3 #
###########
pushd "${build_dir}" > /dev/null 2>&1
build_sqlite3 "${workspace_dir}" "${sqlite_dir}"
popd > /dev/null 2>&1
# Print artifacts
cd "${workspace_dir}"
printf '\n ----------------------- lib/pkgconfig/sqlite3.pc ---------------------- \n'
cat ./lib/pkgconfig/sqlite3.pc
printf '\n ----------------------------------------------------------------------- \n'
ls -lah ./lib ./include ./bin ./share/man/man1
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment