Last active
March 24, 2021 13:36
-
-
Save obradovic/dd428538b72b5e8281cb619ba47f3c82 to your computer and use it in GitHub Desktop.
compile ffmpeg on apple silicon
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/bin/bash | |
set -exuo pipefail | |
# | |
# MODIFIED FROM: https://github.com/ssut/ffmpeg-on-apple-silicon | |
# | |
# WILL NEED THE USER TO ENTER ROOT PW | |
sudo xcode-select -s /Applications/Xcode.app/Contents/Developer | |
# SET UP DIRECTORIES | |
WORKDIR="$(pwd)" | |
DEST_DIR="$WORKDIR/dest" | |
SRC_DIR="$WORKDIR/src" | |
if [[ -e "${SRC_DIR}" ]]; then | |
rm -rf "${SRC_DIR}" | |
fi | |
mkdir -p ${DEST_DIR} | |
mkdir -p ${SRC_DIR} | |
# SET UP ENV VARS | |
export PATH=${DEST_DIR}/bin:$PATH | |
export CC=clang && export PKG_CONFIG_PATH="${DEST_DIR}/lib/pkgconfig" | |
export MACOSX_DEPLOYMENT_TARGET=11.0 | |
export LDFLAGS=${LDFLAGS:-} | |
export CFLAGS=${CFLAGS:-} | |
export NUM_PARALLEL_BUILDS=$(sysctl -n hw.ncpu) | |
export MAKE_INSTALL="make -j $NUM_PARALLEL_BUILDS && make install" | |
export ARCH=x86_64 | |
if [[ "$(uname -m)" == "arm64" ]]; then | |
export ARCH=arm64 | |
fi | |
# INSTALL BREW PACKAGES | |
function ensure_package () { | |
if [[ "$ARCH" == "arm64" ]]; then | |
if [[ ! -e "/opt/homebrew/opt/$1" ]]; then | |
echo "Installing $1 using Homebrew" | |
brew install "$1" | |
export LDFLAGS="-L/opt/homebrew/opt/$1/lib ${LDFLAGS}" | |
export CFLAGS="-I/opt/homebrew/opt/$1/include ${CFLAGS}" | |
export PKG_CONFIG_PATH="$PKG_CONFIG_PATH:/opt/homebrew/opt/$1/lib/pkgconfig" | |
fi | |
else | |
if [[ ! -e "/usr/local/opt/$1" ]]; then | |
echo "Installing $1 using Homebrew" | |
brew install "$1" | |
export LDFLAGS="-L/usr/local/opt/$1/lib ${LDFLAGS}" | |
export CFLAGS="-I/usr/local/opt/$1/include ${CFLAGS}" | |
export PKG_CONFIG_PATH="$PKG_CONFIG_PATH:/usr/local/opt/$1/lib/pkgconfig" | |
fi | |
fi | |
} | |
ensure_package pkgconfig | |
ensure_package libtool | |
ensure_package glib | |
if ! command -v autoreconf &> /dev/null; then | |
brew install autoconf | |
fi | |
if ! command -v automake &> /dev/null; then | |
brew install automake | |
fi | |
if ! command -v cmake &> /dev/null; then | |
brew install cmake | |
fi | |
if ! command -v handbrake &> /dev/null; then | |
echo "Skipping handbrake for now" | |
# brew install --build-from-source handbrake | |
fi | |
# CLONE GIT REPOS | |
echo "Cloning required git repositories" | |
git clone --depth 1 -b master https://code.videolan.org/videolan/x264.git $SRC_DIR/x264 & | |
git clone --depth 1 -b origin https://github.com/rbrito/lame.git $SRC_DIR/lame & | |
git clone --depth 1 -b master https://github.com/webmproject/libvpx $SRC_DIR/libvpx & | |
git clone --depth 1 -b master https://github.com/FFmpeg/FFmpeg $SRC_DIR/ffmpeg & | |
git clone --depth 1 -b v2.0.1 https://aomedia.googlesource.com/aom.git $SRC_DIR/aom & | |
wait | |
# statically linking glibc is discouraged. Dont do this. | |
# echo "Downloading: glib (2.66.2)" | |
# curl -Ls -o - https://download.gnome.org/sources/glib/2.66/glib-2.66.2.tar.xz | tar Jxf - -C $SRC_DIR/ | |
# curl -o "$SRC_DIR/glib-2.66.2/hardcoded-patchs.diff" https://raw.githubusercontent.com/Homebrew/formula-patches/6164294a75541c278f3863b111791376caa3ad26/glib/hardcoded-paths.diff | |
# | |
# HERE IS A CRAPTON OF FUNCTIONS TO DOWNLOAD/INSTALL REQUIRED PACKAGES | |
# | |
function download_src_gz { | |
url=$1 | |
echo "♻️ Downloading: $url" | |
{(curl -Ls -o - $url | tar zxf - -C $SRC_DIR/ )}; | |
} | |
function download_src_xz { | |
url=$1 | |
echo "♻️ Downloading: $url" | |
{(curl -Ls -o - $url | tar Jxf - -C $SRC_DIR/) }; | |
} | |
function download_src_bz { | |
url=$1 | |
echo "♻️ Downloading: $url" | |
{(curl -Ls -o - $url | tar xf - -C $SRC_DIR/) }; | |
} | |
function build_fribidi () { | |
if [[ ! -e "${DEST_DIR}/lib/pkgconfig/fribidi.pc" ]]; then | |
local VERSION=1.0.10 | |
local NAME=fribidi-$VERSION | |
download_src_xz https://github.com/fribidi/fribidi/releases/download/v$VERSION/$NAME.tar.xz | |
echo '♻️ ' Start compiling $NAME | |
cd $SRC_DIR/$NAME | |
./configure --prefix=${DEST_DIR} --disable-debug --disable-dependency-tracking --disable-silent-rules --disable-shared --enable-static | |
$MAKE_INSTALL | |
fi | |
} | |
function build_yasm () { | |
if [[ ! -e "${DEST_DIR}/lib/libyasm.a" ]]; then | |
local VERSION=1.3.0 | |
local NAME=yasm-$VERSION | |
download_src_gz http://www.tortall.net/projects/yasm/releases/$NAME.tar.gz | |
echo '♻️ ' Start compiling $NAME | |
cd $SRC_DIR/$NAME | |
./configure --prefix=${DEST_DIR} | |
$MAKE_INSTALL | |
fi | |
} | |
function build_aom () { | |
if [[ ! -e "${DEST_DIR}/lib/pkgconfig/aom.pc" ]]; then | |
echo '♻️ ' Start compiling AOM | |
cd ${SRC_DIR} | |
cd aom | |
mkdir aom_build | |
cd aom_build | |
AOM_CMAKE_PARAMS="-DENABLE_DOCS=off -DENABLE_EXAMPLES=off -DENABLE_TESTDATA=off -DENABLE_TESTS=off -DENABLE_TOOLS=off -DCMAKE_INSTALL_PREFIX:PATH=${DEST_DIR} -DLIBTYPE=STATIC" | |
if [[ "$ARCH" == "arm64" ]]; then | |
AOM_CMAKE_PARAMS="$AOM_CMAKE_PARAMS -DCONFIG_RUNTIME_CPU_DETECT=0" | |
fi | |
cmake ${SRC_DIR}/aom $AOM_CMAKE_PARAMS | |
$MAKE_INSTALL | |
fi | |
} | |
function build_nasm () { | |
if [[ ! -e "${DEST_DIR}/bin/nasm" ]]; then | |
local VERSION=2.15.05 | |
local NAME=nasm-$VERSION | |
download_src_gz https://www.nasm.us/pub/nasm/releasebuilds/$VERSION/$NAME.tar.gz | |
echo '♻️ ' Start compiling $NAME | |
cd $SRC_DIR/$NAME | |
cd nasm-$VERSION | |
./configure --prefix=${DEST_DIR} | |
$MAKE_INSTALL | |
fi | |
} | |
function build_pkgconfig () { | |
if [[ ! -e "${DEST_DIR}/bin/pkg-config" ]]; then | |
local VERSION=0.29.2 | |
local NAME=pkg-config-$VERSION | |
download_src_gz https://pkg-config.freedesktop.org/releases/$NAME.tar.gz | |
echo '♻️ ' Start compiling $NAME | |
cd $SRC_DIR/$NAME | |
export LDFLAGS="-framework Foundation -framework Cocoa" | |
./configure --prefix=${DEST_DIR} --with-pc-path=${DEST_DIR}/lib/pkgconfig --with-internal-glib --disable-shared --enable-static | |
$MAKE_INSTALL | |
unset LDFLAGS | |
fi | |
} | |
function build_zlib () { | |
if [[ ! -e "${DEST_DIR}/lib/pkgconfig/zlib.pc" ]]; then | |
local VERSION=1.2.11 | |
local NAME=zlib-$VERSION | |
download_src_gz https://zlib.net/$NAME.tar.gz | |
echo '♻️ ' Start compiling $NAME | |
cd $SRC_DIR/$NAME | |
./configure --prefix=${DEST_DIR} | |
$MAKE_INSTALL | |
rm ${DEST_DIR}/lib/libz.so* || true | |
rm ${DEST_DIR}/lib/libz.* || true | |
fi | |
} | |
function build_lame () { | |
if [[ ! -e "${DEST_DIR}/lib/libmp3lame.a" ]]; then | |
cd ${SRC_DIR} | |
cd lame | |
./configure --prefix=${DEST_DIR} --disable-shared --enable-static | |
$MAKE_INSTALL | |
fi | |
} | |
function build_x264 () { | |
if [[ ! -e "${DEST_DIR}/lib/pkgconfig/x264.pc" ]]; then | |
echo '♻️ ' Start compiling X264 | |
cd ${SRC_DIR} | |
cd x264 | |
./configure --prefix=${DEST_DIR} --disable-shared --enable-static --enable-pic | |
$MAKE_INSTALL | |
make install-lib-static | |
fi | |
} | |
function build_x265 () { | |
if [[ ! -e "${DEST_DIR}/lib/pkgconfig/x265.pc" ]]; then | |
local VERSION=3.3 | |
local NAME=x265_$VERSION | |
download_src_gz https://bitbucket.org/multicoreware/x265_git/downloads/$NAME.tar.gz | |
echo '♻️ ' Start compiling $NAME | |
rm -f ${DEST_DIR}/include/x265*.h 2>/dev/null | |
rm -f ${DEST_DIR}/lib/libx265.a 2>/dev/null | |
echo '♻️ ' X265 12bit | |
cd ${SRC_DIR} | |
cd $NAME/source | |
cmake -DCMAKE_INSTALL_PREFIX:PATH=${DEST_DIR} -DHIGH_BIT_DEPTH=ON -DMAIN12=ON -DENABLE_SHARED=NO -DEXPORT_C_API=NO -DENABLE_CLI=OFF . | |
make -j ${NUM_PARALLEL_BUILDS} | |
mv libx265.a libx265_main12.a | |
make clean-generated | |
rm CMakeCache.txt | |
echo '♻️ ' X265 10bit | |
cd ${SRC_DIR} | |
cd $NAME/source | |
cmake -DCMAKE_INSTALL_PREFIX:PATH=${DEST_DIR} -DMAIN10=ON -DHIGH_BIT_DEPTH=ON -DENABLE_SHARED=NO -DEXPORT_C_API=NO -DENABLE_CLI=OFF . | |
make clean | |
make -j ${NUM_PARALLEL_BUILDS} | |
mv libx265.a libx265_main10.a | |
make clean-generated && rm CMakeCache.txt | |
echo '♻️ ' X265 full | |
cd ${SRC_DIR} | |
cd $NAME/source | |
cmake -DCMAKE_INSTALL_PREFIX:PATH=${DEST_DIR} -DEXTRA_LIB="x265_main10.a;x265_main12.a" -DEXTRA_LINK_FLAGS=-L. -DLINKED_12BIT=ON -DLINKED_10BIT=ON -DENABLE_SHARED=OFF -DENABLE_CLI=OFF . | |
make clean | |
make -j ${NUM_PARALLEL_BUILDS} | |
mv libx265.a libx265_main.a | |
libtool -static -o libx265.a libx265_main.a libx265_main10.a libx265_main12.a 2>/dev/null | |
make install | |
fi | |
} | |
function build_vpx () { | |
if [[ ! -e "${DEST_DIR}/lib/pkgconfig/vpx.pc" ]]; then | |
echo '♻️ ' Start compiling VPX | |
cd ${SRC_DIR} | |
cd libvpx | |
./configure --prefix=${DEST_DIR} --enable-vp8 --enable-postproc --enable-vp9-postproc --enable-vp9-highbitdepth --disable-examples --disable-docs --enable-multi-res-encoding --disable-unit-tests --enable-pic --disable-shared | |
$MAKE_INSTALL | |
fi | |
} | |
function build_expat () { | |
if [[ ! -e "${DEST_DIR}/lib/pkgconfig/expat.pc" ]]; then | |
local VERSION=2.2.10 | |
local NAME=expat-$VERSION | |
download_src_gz https://github.com/libexpat/libexpat/releases/download/R_`echo $VERSION | tr "." "_"`/$NAME.tar.gz | |
echo '♻️ ' Start compiling $NAME | |
cd $SRC_DIR/$NAME | |
./configure --prefix=${DEST_DIR} --disable-shared --enable-static | |
$MAKE_INSTALL | |
fi | |
} | |
function build_libiconv () { | |
if [[ ! -e "${DEST_DIR}/lib/libiconv.a" ]]; then | |
local VERSION=1.16 | |
local NAME=libiconv-$VERSION | |
download_src_gz https://ftp.gnu.org/pub/gnu/libiconv/$NAME.tar.gz | |
echo '♻️ ' Start compiling $NAME | |
cd $SRC_DIR/$NAME | |
./configure --prefix=${DEST_DIR} --disable-shared --enable-static | |
$MAKE_INSTALL | |
fi | |
} | |
function build_enca () { | |
if [[ ! -d "${DEST_DIR}/libexec/enca" ]]; then | |
local VERSION=1.19 | |
local NAME=enca-$VERSION | |
download_src_gz https://dl.cihar.com/enca/$NAME.tar.gz | |
echo '♻️ ' Start compiling $NAME | |
cd $SRC_DIR/$NAME | |
./configure --prefix=${DEST_DIR} --disable-dependency-tracking --disable-shared --enable-static | |
$MAKE_INSTALL | |
fi | |
} | |
function build_freetype () { | |
if [[ ! -e "${DEST_DIR}/lib/pkgconfig/freetype2.pc" ]]; then | |
local VERSION=2.10.4 | |
local NAME=freetype-$VERSION | |
download_src_gz https://download.savannah.gnu.org/releases/freetype/$NAME.tar.gz | |
echo '♻️ ' Start compiling $NAME | |
cd $SRC_DIR/$NAME | |
./configure --prefix=${DEST_DIR} --disable-shared --enable-static | |
$MAKE_INSTALL | |
fi | |
} | |
function build_gettext () { | |
if [[ ! -e "${DEST_DIR}/lib/pkgconfig/gettext.pc" ]]; then | |
local VERSION=0.21 | |
local NAME=gettext-$VERSION | |
download_src_xz https://ftp.gnu.org/gnu/gettext/$NAME.tar.xz | |
echo '♻️ ' Start compiling $NAME | |
cd $SRC_DIR/$NAME | |
./configure --prefix=${DEST_DIR} --disable-dependency-tracking --disable-silent-rules --disable-debug --disable-shared --enable-static \ | |
--with-included-gettext --with-included-glib --with-includedlibcroco --with-included-libunistring --with-emacs \ | |
--disable-java --disable-csharp --without-git --without-cvs --without-xz | |
$MAKE_INSTALL | |
fi | |
} | |
function build_fontconfig () { | |
if [[ ! -e "${DEST_DIR}/lib/pkgconfig/fontconfig.pc" ]]; then | |
local VERSION=2.13.93 | |
local NAME=fontconfig-$VERSION | |
download_src_gz https://www.freedesktop.org/software/fontconfig/release/$NAME.tar.gz | |
echo '♻️ ' Start compiling $NAME | |
cd $SRC_DIR/$NAME | |
./configure --prefix=${DEST_DIR} --enable-iconv --disable-libxml2 --disable-shared --enable-static --disable-docs | |
$MAKE_INSTALL | |
fi | |
} | |
function build_harfbuzz () { | |
if [[ ! -e "${DEST_DIR}/lib/pkgconfig/harfbuzz.pc" ]]; then | |
local VERSION=2.7.2 | |
local NAME=harfbuzz-$VERSION | |
download_src_xz https://github.com/harfbuzz/harfbuzz/releases/download/$VERSION/$NAME.tar.xz | |
echo '♻️ ' Start compiling harfbuzz | |
cd $SRC_DIR/harfbuzz-$NAME | |
./configure --prefix=${DEST_DIR} --disable-shared --enable-static | |
$MAKE_INSTALL | |
fi | |
} | |
function build_ass () { | |
if [[ ! -e "${DEST_DIR}/lib/pkgconfig/libass.pc" ]]; then | |
local VERSION=0.15.0 | |
local NAME=libass-$VERSION | |
download_src_gz https://github.com/libass/libass/releases/download/$VERSION/$NAME.tar.gz | |
cd $SRC_DIR/$NAME | |
autoreconf -i | |
./configure --prefix=${DEST_DIR} --disable-dependency-tracking --disable-shread --enable-static | |
$MAKE_INSTALL | |
fi | |
} | |
function build_opus () { | |
if [[ ! -e "${DEST_DIR}/lib/pkgconfig/opus.pc" ]]; then | |
local VERSION=1.3.1 | |
local NAME=opus-$VERSION | |
download_src_gz https://archive.mozilla.org/pub/opus/$NAME.tar.gz | |
echo '♻️ ' Start compiling $NAME | |
cd $SRC_DIR/$NAME | |
./configure --prefix=${DEST_DIR} --disable-shared --enable-static | |
$MAKE_INSTALL | |
fi | |
} | |
function build_ogg () { | |
if [[ ! -e "${DEST_DIR}/lib/pkgconfig/ogg.pc" ]]; then | |
local VERSION=1.3.4 | |
local NAME=libogg-$VERSION | |
download_src_gz https://downloads.xiph.org/releases/ogg/$NAME.tar.gz | |
curl -s -o "$SRC_DIR/$NAME/fix_unsigned_typedefs.patch" "https://github.com/xiph/ogg/commit/c8fca6b4a02d695b1ceea39b330d4406001c03ed.patch?full_index=1" | |
echo '♻️ ' Start compiling $NAME | |
cd $SRC_DIR/$NAME | |
patch -p1 < ./fix_unsigned_typedefs.patch | |
./configure --prefix=${DEST_DIR} --disable-shared --enable-static | |
$MAKE_INSTALL | |
fi | |
} | |
function build_vorbis () { | |
if [[ ! -e "${DEST_DIR}/lib/pkgconfig/vorbis.pc" ]]; then | |
local VERSION=1.3.7 | |
local NAME=libvorbis-$VERSION | |
download_src_gz https://downloads.xiph.org/releases/vorbis/$NAME.tar.gz | |
echo '♻️ ' Start compiling $NAME | |
cd $SRC_DIR/$NAME | |
./configure --prefix=${DEST_DIR} --with-ogg-libraries=${DEST_DIR}/lib --with-ogg-includes=${DEST_DIR}/include/ --enable-static --disable-shared --build=x86_64 | |
$MAKE_INSTALL | |
fi | |
} | |
function build_theora () { | |
if [[ ! -e "${DEST_DIR}/lib/pkgconfig/theora.pc" ]]; then | |
local VERSION=1.1.1 | |
local NAME=libtheora-$VERSION | |
download_src_bz http://downloads.xiph.org/releases/theora/$NAME.tar.bz2 | |
echo '♻️ ' Start compiling $NAME | |
cd $SRC_DIR/$NAME | |
./configure --prefix=${DEST_DIR} --disable-asm --with-ogg-libraries=${DEST_DIR}/lib --with-ogg-includes=${DEST_DIR}/include/ --with-vorbis-libraries=${DEST_DIR}/lib --with-vorbis-includes=${DEST_DIR}/include/ --enable-static --disable-shared | |
$MAKE_INSTALL | |
fi | |
} | |
function build_vidstab () { | |
if [[ ! -e "${DEST_DIR}/lib/pkgconfig/vidstab.pc" ]]; then | |
local VERSION=1.1.0 | |
local NAME=vid.stab-$VERSION | |
download_src_gz https://github.com/georgmartius/vid.stab/archive/v$VERSION.tar.gz | |
curl -s -o "$SRC_DIR/$NAME/fix_cmake_quoting.patch" https://raw.githubusercontent.com/Homebrew/formula-patches/5bf1a0e0cfe666ee410305cece9c9c755641bfdf/libvidstab/fix_cmake_quoting.patch | |
echo '♻️ ' Start compiling $NAME | |
cd $SRC_DIR/$NAME | |
patch -p1 < fix_cmake_quoting.patch | |
cmake . -DCMAKE_INSTALL_PREFIX:PATH=${DEST_DIR} -DLIBTYPE=STATIC -DBUILD_SHARED_LIBS=OFF -DUSE_OMP=OFF -DENABLE_SHARED=off | |
$MAKE_INSTALL | |
fi | |
} | |
function build_snappy () { | |
if [[ ! -e "${DEST_DIR}/lib/libsnappy.a" ]]; then | |
local VERSION=1.1.8 | |
local NAME=snappy-$VERSION | |
download_src_gz https://github.com/google/snappy/archive/$VERSION.tar.gz | |
echo '♻️ ' Start compiling $NAME | |
cd $SRC_DIR/$NAME | |
cmake . -DCMAKE_INSTALL_PREFIX:PATH=${DEST_DIR} -DLIBTYPE=STATIC -DENABLE_SHARED=off | |
$MAKE_INSTALL | |
fi | |
} | |
function build_libpng { | |
if [[ ! -e "${DEST_DIR}/lib/libpng.a" ]]; then | |
# local VERSION=1.6.37 $ libtheora borks with this version cause it cant find png_sizeof | |
local VERSION=1.2.59 # This is the last version that has png_sizeof. Was deprecated as of 1.4 | |
local NAME=libpng-$VERSION | |
download_src_gz https://sourceforge.net/projects/libpng/files/libpng`echo $VERSION | cut -d'.' -f1,2 | tr -d '.'`/$VERSION/$NAME.tar.gz/download | |
cd $SRC_DIR/$NAME | |
./configure --prefix=${DEST_DIR} | |
$MAKE_INSTALL | |
# NOTE: ALSO install in the global dir cause somehow we cant configure libfontconfig to find it anywhere else | |
./configure | |
make -j ${NUM_PARALLEL_BUILDS} | |
sudo make install | |
fi | |
} | |
function build_ffmpeg () { | |
echo '♻️ ' Start compiling FFMPEG | |
cd $SRC_DIR/ffmpeg | |
export LDFLAGS="-L${DEST_DIR}/lib ${LDFLAGS:-}" | |
export CFLAGS="-I${DEST_DIR}/include ${CFLAGS:-}" | |
export LDFLAGS="$LDFLAGS -lexpat -lenca -lfribidi -liconv -lstdc++ -lfreetype -framework CoreText -framework VideoToolbox" | |
./configure --prefix=${DEST_DIR} --extra-cflags="-fno-stack-check" --arch=${ARCH} --cc=/usr/bin/clang \ | |
--enable-fontconfig --enable-gpl --enable-libopus --enable-libtheora --enable-libvorbis \ | |
--enable-libmp3lame --enable-libass --enable-libfreetype --enable-libx264 --enable-libx265 --enable-libvpx \ | |
--enable-libaom --enable-libvidstab --enable-libsnappy --enable-version3 --pkg-config-flags=--static \ | |
--disable-ffplay --enable-postproc --enable-nonfree --enable-runtime-cpudetect | |
echo "build start" | |
start_time="$(date -u +%s)" | |
$MAKE_INSTALL | |
end_time="$(date -u +%s)" | |
elapsed="$(($end_time-$start_time))" | |
echo "[FFmpeg] $elapsed seconds elapsed for build" | |
} | |
# | |
# INSTALL THE REQUIRED PACKAGES, IN ROUGH ORDER | |
# | |
build_fribidi | |
build_yasm | |
build_aom | |
build_nasm | |
build_pkgconfig | |
build_zlib | |
build_lame | |
build_x264 | |
build_x265 | |
build_vpx | |
build_expat | |
build_libiconv | |
build_enca | |
build_freetype | |
if [[ "$ARCH" == "arm64" ]]; then | |
build_gettext | |
fi | |
build_libpng | |
build_fontconfig | |
build_harfbuzz | |
build_ass | |
build_opus | |
build_ogg | |
build_vorbis | |
build_theora | |
build_vidstab | |
build_snappy | |
build_ffmpeg |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment