[ENG-1184, ENG-1286, ENG-1330] Rework native dependencies (+ deb fixes) (#1685)

* Almost working

* Downgrade libplacebo
 - FFMpeg 6.0 uses some now removed deprecated functions

* Use -Oz for zimg

* Fix CI script to run the new ffmpeg build script

* Fix heif step name + Ignore docker cache while building in CI

* Fix Opencl build on linux

* Fix adding incorrect -target argument to linker
 - Update zig for windows target

* Disable opengl for ffmpeg, it only uses it as an outdev, not for processing
 - Disable opengl and directx for libplacebo, ffmpeg only supports vulkan when using it
 - Add WIN32_LEAN_AND_MEAN to global cflags to optimize windows api usage
 - Fix 99-heif.sh incorrect bsdtar flag

* Remove WIN32_LEAN_AND_MEAN from global CFLAGS as that was breaking OpenCL build
 - Fix Dockerfile step for cleaning up the out dir
 - Improve licensing handling

* x86_64 windows and linux builds are working

* Fix aarch64 build for windows and linux

* Fix symbol visibility in linux builds
 - Fix soxr failing to download due to sourcefourge
 - Only patch zimg on windows targets
 - Tell cmake to hide libheif symbols

* Fix Linux .so rpath
 - Add lzo dependency
 - Publish source for the built libs
 - Add warning for missing nasm in tauri.mjs
 - Remove ffmpeg install from setup.sh
 - Add download logic for our linux ffmpeg bundle in preprep.mjs

* Remove jobs, docker doesn't support this

* Fix typing

* Change ffmpeg references to native deps
 - Rename FFMpeg.framework to Spacedrive.framework
 - Centralize the macOS native deps build with the windows and linux one
 - Change the preprep script to only download our native deps
 - Remove old macOS ffmpeg build scripts

* Compress native deps before creating github artifact
 - The zip implementation for github artifact does not mantain symlinks and permissions
 - Remove conditional protoc, it is now always included

* Don't strip dylibs, it was breaking them
 - Only download macOS Framework for darwin targets
 - Fix preprep script
 - Improve README.md for native-deps
 - Fix not finding native-deps src

* Attempt to fix macOS dylib

* Fix macOS dylibs
 - Replace lld.ld64 with apple's own linker
 - Add stages for building apple's compiler tools to use instead of LLVM ones

* Ensure sourced file exists

* All targets should build now
 - Fix environment sourcing in build.sh
 - Some minor improvements to cc.sh
 - Fix incorrect flag in zlib.sh
 - Improve how -f[...] flags are passed to compiler and linker
 - Add more stack hardening flags

* We now can support macOS 11.0 on arm64

* Improve macOS Framework generation
 - Remove installed unused deps
 - Improve cleanup and organization logic in Dockerfile last step
 - Move libav* .dll.a to .lib to fix missing files in windows target
 - Remove apple tools from /srv folder after installation to prevent their files from being copied by other stage steps
 - Create all the necessary symlinks for the macOS targets while building
 - Remove symlink logic for macOS target from preprep.mjs

* Remove native-deps from spacedrive repo
 - It now resides in https://github.com/spacedriveapp/native-deps
 - Modify preprep script to dowload native-deps from new location
 - Remove Github API code from scripts (not needed anymore)
 - Add flock.mjs to allow running tauri.mjs cleanup as soon as cargo finishes building in linux

* Handle flock not present in system
 - Allow macOS to try using flock

* Fix preprep on macOS

* Add script that patch deb to fix errors and warnings raised by lintian

* Fix ctrl+c/ctrl+v typo

* Remove gstreamer1.0-gtk3 from deb dependencies

* eval is evil

* Handle tauri build release with an explicit target in fix-deb.sh

* Preserve environment variables when re-executing fix-deb with sudo

* Only execute fix-deb.sh when building a deb bundle

* Improvements fix-deb.sh

* Improve setup.sh (Add experiemental alpine support)
This commit is contained in:
Vítor Vasconcellos
2023-11-17 16:20:14 -03:00
committed by GitHub
parent c2dd3661f9
commit 48afea5a4b
63 changed files with 548 additions and 3378 deletions

172
scripts/fix-deb.sh Executable file
View File

@@ -0,0 +1,172 @@
#!/usr/bin/env bash
set -eEuo pipefail
if [ "${CI:-}" = "true" ]; then
set -x
fi
if [ "$(id -u)" -ne 0 ]; then
echo "This script requires root privileges." >&2
exec sudo -E env _UID="$(id -u)" _GID="$(id -g)" "$0" "$@"
fi
echo "Fixing deb bundle..." >&2
umask 0
err() {
for _line in "$@"; do
echo "$_line" >&2
done
exit 1
}
has() {
for prog in "$@"; do
if ! command -v "$prog" 1>/dev/null 2>&1; then
return 1
fi
done
}
if ! has tar curl gzip strip; then
err 'Dependencies missing.' \
"This script requires 'tar', 'curl', 'gzip' and 'strip' to be installed and available on \$PATH."
fi
# Go to script root
CDPATH='' cd -- "$(dirname "$0")"
_root="$(pwd -P)"
if [ -n "${TARGET:-}" ]; then
cd "../target/${TARGET}/release/bundle/deb" || err 'Failed to find deb bundle'
else
cd ../target/release/bundle/deb || err 'Failed to find deb bundle'
fi
# Find deb file with the highest version number, name format: spacedrive_<version>_<arch>.deb
_deb="$(find . -type f -name '*.deb' | sort -t '_' -k '2,2' -V | tail -n 1)"
# Clean up build unused artifacts
rm -rf "$(basename "$_deb" .deb)"
# Make a backup of deb
cp "$_deb" "$_deb.bak"
# Temporary directory
_tmp="$(mktemp -d)"
cleanup() {
_err=$?
rm -rf "$_tmp"
# Restore backed up deb if something goes wrong
if [ $_err -ne 0 ]; then
mv "${_deb:?}.bak" "$_deb"
fi
# Ensure deb owner is the same as the user who ran the script
chown "${_UID:-0}:${_GID:-0}" "$_deb" 2>/dev/null || true
rm -f "${_deb:?}.bak"
exit "$_err"
}
trap 'cleanup' EXIT
# Extract deb to a tmp dir
ar x "$_deb" --output="$_tmp"
# Extract data.tar.xz
mkdir -p "${_tmp}/data"
tar -xzf "${_tmp}/data.tar.gz" -C "${_tmp}/data"
# Extract control.tar.xz
mkdir -p "${_tmp}/control"
tar -xzf "${_tmp}/control.tar.gz" -C "${_tmp}/control"
# Fix files owner
chown -R root:root "$_tmp"
# Create doc directory
mkdir -p "$_tmp"/data/usr/share/{doc/spacedrive,man/man1}
# Create changelog.gz
curl -LSs 'https://gist.githubusercontent.com/HeavenVolkoff/0993c42bdb0b952eb5bf765398e9b921/raw/changelog' \
| gzip -9 >"${_tmp}/data/usr/share/doc/spacedrive/changelog.gz"
# Copy LICENSE to copyright
cp "${_root}/../LICENSE" "${_tmp}/data/usr/share/doc/spacedrive/copyright"
# Copy dependencies licenses
(
for _license in "${_root}"/../apps/.deps/licenses/*; do
cat <<EOF
$(basename "$_license"):
$(cat "$_license")
===============================================================================
EOF
done
) | gzip -9 >"${_tmp}/data/usr/share/doc/spacedrive/thrid-party-licenses.gz"
# Create manual page
curl -LSs 'https://gist.githubusercontent.com/HeavenVolkoff/0993c42bdb0b952eb5bf765398e9b921/raw/spacedrive.1' \
| gzip -9 >"${_tmp}/data/usr/share/man/man1/spacedrive.1.gz"
# Fill the Categories entry in .desktop file
sed -i 's/^Categories=.*/Categories=System;FileTools;FileManager;/' "${_tmp}/data/usr/share/applications/spacedrive.desktop"
# Fix data permissions
find "${_tmp}/data" -type d -exec chmod 755 {} +
find "${_tmp}/data" -type f -exec chmod 644 {} +
# Fix main executable permission
chmod 755 "${_tmp}/data/usr/bin/spacedrive"
# Make generic named shared libs symlinks to the versioned ones
find "${_tmp}/data/usr/lib" -type f -name '*.so.*' -exec sh -euc \
'for _lib in "$@"; do _link="$_lib" && while { _link="${_link%.*}" && [ "$_link" != "${_lib%.so*}" ]; }; do if [ -f "$_link" ]; then ln -sf "$(basename "$_lib")" "$_link"; fi; done; done' \
sh {} +
# Strip all executables and shared libs
find "${_tmp}/data/usr/bin" "${_tmp}/data/usr/lib" -type f -exec strip --strip-unneeded {} \;
# Add Section field to control file, if it doesnt exists
if ! grep -q '^Section:' "${_tmp}/control/control"; then
echo 'Section: contrib/utils' >>"${_tmp}/control/control"
fi
# Add Recommends field to control file after Depends field
_recomends='gstreamer1.0-plugins-ugly'
if grep -q '^Recommends:' "${_tmp}/control/control"; then
sed -i "s/^Recommends:.*/Recommends: ${_recomends}/" "${_tmp}/control/control"
else
sed -i "/^Depends:/a Recommends: ${_recomends}" "${_tmp}/control/control"
fi
# Add Suggests field to control file after Recommends field
_suggests='gstreamer1.0-plugins-bad'
if grep -q '^Suggests:' "${_tmp}/control/control"; then
sed -i "s/^Suggests:.*/Suggests: ${_suggests}/" "${_tmp}/control/control"
else
sed -i "/^Recommends:/a Suggests: ${_suggests}" "${_tmp}/control/control"
fi
# Re-calculate md5sums
(cd "${_tmp}/data" && find . -type f -exec md5sum {} + >"${_tmp}/control/md5sums")
# Fix control files permission
find "${_tmp}/control" -type f -exec chmod 644 {} +
# Compress data.tar.xz
tar -czf "${_tmp}/data.tar.gz" -C "${_tmp}/data" .
# Compress control.tar.xz
tar -czf "${_tmp}/control.tar.gz" -C "${_tmp}/control" .
# Compress deb
ar rcs "$_deb" "${_tmp}/debian-binary" "${_tmp}/control.tar.gz" "${_tmp}/data.tar.gz"

69
scripts/preprep.mjs Normal file → Executable file
View File

@@ -1,18 +1,17 @@
#!/usr/bin/env node
import * as fs from 'node:fs/promises'
import * as path from 'node:path'
import { env, exit, umask } from 'node:process'
import { fileURLToPath } from 'node:url'
import { extractTo } from 'archive-wasm/src/fs.mjs'
import * as _mustache from 'mustache'
import { downloadFFMpeg, downloadLibHeif, downloadPDFium, downloadProtc } from './utils/deps.mjs'
import { getGitBranches } from './utils/git.mjs'
import { getConst, NATIVE_DEPS_URL, NATIVE_DEPS_ASSETS } from './utils/consts.mjs'
import { get } from './utils/fetch.mjs'
import { getMachineId } from './utils/machineId.mjs'
import {
setupMacOsFramework,
symlinkSharedLibsMacOS,
symlinkSharedLibsLinux,
} from './utils/shared.mjs'
import { symlinkSharedLibsMacOS, symlinkSharedLibsLinux } from './utils/shared.mjs'
import { which } from './utils/which.mjs'
if (/^(msys|mingw|cygwin)$/i.test(env.OSTYPE ?? '')) {
@@ -57,43 +56,26 @@ packages/scripts/${machineId[0] === 'Windows_NT' ? 'setup.ps1' : 'setup.sh'}
// Directory where the native deps will be downloaded
const nativeDeps = path.join(__root, 'apps', '.deps')
await fs.rm(nativeDeps, { force: true, recursive: true })
await Promise.all(
['bin', 'lib', 'include'].map(dir =>
fs.mkdir(path.join(nativeDeps, dir), { mode: 0o750, recursive: true })
)
)
await fs.mkdir(nativeDeps, { mode: 0o750, recursive: true })
// Accepted git branches for querying for artifacts (current, main, master)
const branches = await getGitBranches(__root)
try {
console.log('Downloading Native dependencies...')
// Download all necessary external dependencies
await Promise.all([
downloadProtc(machineId, nativeDeps).catch(e => {
console.error(
'Failed to download protobuf compiler, this is required to build Spacedrive. ' +
'Please install it with your system package manager'
)
throw e
}),
downloadPDFium(machineId, nativeDeps).catch(e => {
console.warn(
'Failed to download pdfium lib. ' +
"This is optional, but if one isn't present Spacedrive won't be able to generate thumbnails for PDF files"
)
if (__debug) console.error(e)
}),
downloadFFMpeg(machineId, nativeDeps, branches).catch(e => {
console.error(`Failed to download ffmpeg. ${bugWarn}`)
throw e
}),
downloadLibHeif(machineId, nativeDeps, branches).catch(e => {
console.error(`Failed to download libheif. ${bugWarn}`)
throw e
}),
]).catch(e => {
const assetName = getConst(NATIVE_DEPS_ASSETS, machineId)
if (assetName == null) throw new Error('NO_ASSET')
const archiveData = await get(`${NATIVE_DEPS_URL}/${assetName}`)
await extractTo(archiveData, nativeDeps, {
chmod: 0o600,
recursive: true,
overwrite: true,
})
} catch (e) {
console.error(`Failed to download native dependencies. ${bugWarn}`)
if (__debug) console.error(e)
exit(1)
})
}
// Extra OS specific setup
try {
@@ -104,14 +86,9 @@ try {
throw e
})
} else if (machineId[0] === 'Darwin') {
console.log(`Setup Framework...`)
await setupMacOsFramework(nativeDeps).catch(e => {
console.error(`Failed to setup Framework. ${bugWarn}`)
throw e
})
// This is still required due to how ffmpeg-sys-next builds script works
console.log(`Symlink shared libs...`)
await symlinkSharedLibsMacOS(nativeDeps).catch(e => {
await symlinkSharedLibsMacOS(__root, nativeDeps).catch(e => {
console.error(`Failed to symlink shared libs. ${bugWarn}`)
throw e
})

View File

@@ -21,6 +21,14 @@ has() {
done
}
sudo() {
if [ "$(id -u)" -eq 0 ]; then
"$@"
else
env sudo "$@"
fi
}
script_failure() {
if [ -n "${1:-}" ]; then
_line="on line $1"
@@ -58,9 +66,6 @@ if [ "${CI:-}" != "true" ]; then
'https://rustup.rs'
fi
echo "Installing Rust tools..."
cargo install cargo-watch
echo
fi
@@ -134,19 +139,14 @@ case "$(uname)" in
set -- build-essential curl wget file patchelf openssl libssl-dev libgtk-3-dev librsvg2-dev \
libwebkit2gtk-4.0-dev libayatana-appindicator3-dev
# FFmpeg dependencies
set -- "$@" ffmpeg libavcodec-dev libavdevice-dev libavfilter-dev libavformat-dev \
libavutil-dev libswscale-dev libswresample-dev
# Webkit2gtk requires gstreamer plugins for video playback to work
set -- "$@" gstreamer1.0-alsa gstreamer1.0-gl gstreamer1.0-gtk3 gstreamer1.0-libav \
gstreamer1.0-pipewire gstreamer1.0-plugins-bad gstreamer1.0-plugins-base \
gstreamer1.0-plugins-good gstreamer1.0-plugins-ugly gstreamer1.0-pulseaudio \
gstreamer1.0-vaapi libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev \
libgstreamer-plugins-bad1.0-dev
set -- "$@" gstreamer1.0-plugins-good gstreamer1.0-plugins-ugly libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev
# C/C++ build dependencies, required to build some *-sys crates
set -- "$@" llvm-dev libclang-dev clang nasm
set -- "$@" llvm-dev libclang-dev clang nasm perl
# React dependencies
set -- "$@" libvips42
sudo apt-get -y update
sudo apt-get -y install "$@"
@@ -157,15 +157,11 @@ case "$(uname)" in
# Tauri dependencies
set -- base-devel curl wget file patchelf openssl gtk3 librsvg webkit2gtk libayatana-appindicator
# FFmpeg dependencies
set -- "$@" ffmpeg
# Webkit2gtk requires gstreamer plugins for video playback to work
set -- "$@" gst-libav gst-plugins-bad gst-plugins-base gst-plugins-good gst-plugins-ugly \
gst-plugin-pipewire gstreamer-vaapi
set -- "$@" gst-plugins-base gst-plugins-good gst-plugins-ugly
# C/C++ build dependencies, required to build some *-sys crates
set -- "$@" clang nasm
set -- "$@" clang nasm perl
# React dependencies
set -- "$@" libvips
@@ -176,7 +172,7 @@ case "$(uname)" in
echo "Installing dependencies with dnf..."
# For Enterprise Linux, you also need "Development Tools" instead of "C Development Tools and Libraries"
if ! { sudo dnf group install "C Development Tools and Libraries" || sudo sudo dnf group install "Development Tools"; }; then
if ! { sudo dnf group install "C Development Tools and Libraries" || sudo dnf group install "Development Tools"; }; then
err 'We were unable to install the "C Development Tools and Libraries"/"Development Tools" package.' \
'Please open an issue if you feel that this is incorrect.' \
'https://github.com/spacedriveapp/spacedrive/issues'
@@ -190,26 +186,38 @@ case "$(uname)" in
fi
# Tauri dependencies
set -- openssl curl wget file patchelf libappindicator-gtk3-devel librsvg2-devel
set -- openssl openssl-dev curl wget file patchelf libappindicator-gtk3-devel librsvg2-devel
# Webkit2gtk requires gstreamer plugins for video playback to work
set -- "$@" gstreamer1-devel gstreamer1-plugins-base-devel \
gstreamer1-plugins-good gstreamer1-plugins-good-gtk \
gstreamer1-plugins-good-extras gstreamer1-plugins-ugly-free \
gstreamer1-plugins-bad-free gstreamer1-plugins-bad-free-devel \
gstreamer1-plugins-bad-free-extras
set -- "$@" gstreamer1-devel gstreamer1-plugins-base-devel gstreamer1-plugins-good \
gstreamer1-plugins-good-extras gstreamer1-plugins-ugly-free
# C/C++ build dependencies, required to build some *-sys crates
set -- "$@" clang clang-devel nasm
set -- "$@" clang clang-devel nasm perl-core
# React dependencies
set -- "$@" vips
sudo dnf install "$@"
elif has apk; then
echo "Detected apk!"
echo "Installing dependencies with apk..."
echo "Alpine suport is experimental" >&2
# FFmpeg dependencies
if ! sudo dnf install ffmpeg ffmpeg-devel; then
err 'We were unable to install the FFmpeg and FFmpeg-devel packages.' \
'This is likely because the RPM Fusion free repository is not enabled.' \
'https://docs.fedoraproject.org/en-US/quick-docs/setup_rpmfusion'
fi
# Tauri dependencies
set -- build-base curl wget file patchelf openssl-dev gtk+3.0-dev librsvg-dev \
webkit2gtk-dev libayatana-indicator-dev
# Webkit2gtk requires gstreamer plugins for video playback to work
set -- "$@" gst-plugins-base-dev gst-plugins-good gst-plugins-ugly
# C/C++ build dependencies, required to build some *-sys crates
set -- "$@" llvm16-dev clang16 nasm perl
# React dependencies
set -- "$@" vips
sudo apk add "$@"
else
if has lsb_release; then
_distro="'$(lsb_release -s -d)' "
@@ -226,4 +234,7 @@ case "$(uname)" in
;;
esac
echo "Installing Rust tools..."
cargo install cargo-watch
echo 'Your machine has been setup for Spacedrive development!'

119
scripts/tauri.mjs Normal file → Executable file
View File

@@ -1,10 +1,14 @@
#!/usr/bin/env node
import * as fs from 'node:fs/promises'
import * as path from 'node:path'
import { env, exit, umask, platform } from 'node:process'
import { setTimeout } from 'node:timers/promises'
import { fileURLToPath } from 'node:url'
import * as toml from '@iarna/toml'
import { waitLockUnlock } from './utils/flock.mjs'
import { patchTauri } from './utils/patchTauri.mjs'
import spawn from './utils/spawn.mjs'
@@ -53,11 +57,38 @@ if (cargoConfig.env && typeof cargoConfig.env === 'object')
// Default command
if (args.length === 0) args.push('build')
const targets = args
.filter((_, index, args) => {
if (index === 0) return false
const previous = args[index - 1]
return previous === '-t' || previous === '--target'
})
.flatMap(target => target.split(','))
const bundles = args
.filter((_, index, args) => {
if (index === 0) return false
const previous = args[index - 1]
return previous === '-b' || previous === '--bundles'
})
.flatMap(target => target.split(','))
let code = 0
try {
switch (args[0]) {
case 'dev': {
__cleanup.push(...(await patchTauri(__root, nativeDeps, args)))
__cleanup.push(...(await patchTauri(__root, nativeDeps, targets, bundles, args)))
switch (process.platform) {
case 'darwin':
case 'linux':
void waitLockUnlock(path.join(__root, 'target', 'debug', '.cargo-lock')).then(
() => setTimeout(1000).then(cleanUp),
() => {}
)
break
}
break
}
case 'build': {
@@ -65,68 +96,48 @@ try {
env.NODE_OPTIONS = `--max_old_space_size=4096 ${env.NODE_OPTIONS ?? ''}`
}
__cleanup.push(...(await patchTauri(__root, nativeDeps, args)))
__cleanup.push(...(await patchTauri(__root, nativeDeps, targets, bundles, args)))
switch (process.platform) {
case 'darwin': {
// Configure DMG background
env.BACKGROUND_FILE = path.resolve(
desktopApp,
'src-tauri',
'dmg-background.png'
if (process.platform === 'darwin') {
// Configure DMG background
env.BACKGROUND_FILE = path.resolve(desktopApp, 'src-tauri', 'dmg-background.png')
env.BACKGROUND_FILE_NAME = path.basename(env.BACKGROUND_FILE)
env.BACKGROUND_CLAUSE = `set background picture of opts to file ".background:${env.BACKGROUND_FILE_NAME}"`
if (!(await exists(env.BACKGROUND_FILE)))
console.warn(
`WARNING: DMG background file not found at ${env.BACKGROUND_FILE}`
)
env.BACKGROUND_FILE_NAME = path.basename(env.BACKGROUND_FILE)
env.BACKGROUND_CLAUSE = `set background picture of opts to file ".background:${env.BACKGROUND_FILE_NAME}"`
if (!(await exists(env.BACKGROUND_FILE)))
console.warn(
`WARNING: DMG background file not found at ${env.BACKGROUND_FILE}`
)
break
}
case 'linux':
// Cleanup appimage bundle to avoid build_appimage.sh failing
await fs.rm(path.join(__root, 'target', 'release', 'bundle', 'appimage'), {
recursive: true,
force: true,
})
break
break
}
}
}
await spawn('pnpm', ['exec', 'tauri', ...args], desktopApp).catch(async error => {
if (args[0] === 'build' || platform === 'linux') {
// Work around appimage buindling not working sometimes
const appimageDir = path.join(__root, 'target', 'release', 'bundle', 'appimage')
if (
(await exists(path.join(appimageDir, 'build_appimage.sh'))) &&
(await fs.readdir(appimageDir).then(f => f.every(f => !f.endsWith('.AppImage'))))
) {
// Remove AppDir to allow build_appimage to rebuild it
await fs.rm(path.join(appimageDir, 'spacedrive.AppDir'), {
recursive: true,
force: true,
})
return spawn('bash', ['build_appimage.sh'], appimageDir).catch(exitCode => {
code = exitCode
console.error(`tauri ${args[0]} failed with exit code ${exitCode}`)
})
await spawn('pnpm', ['exec', 'tauri', ...args], desktopApp)
if (args[0] === 'build' && bundles.some(bundle => bundle === 'deb' || bundle === 'all')) {
const linuxTargets = targets.filter(target => target.includes('-linux-'))
if (linuxTargets.length > 0)
for (const target of linuxTargets) {
env.TARGET = target
await spawn(path.join(__dirname, 'fix-deb.sh'), [], __dirname)
}
}
console.error(
`tauri ${args[0]} failed with exit code ${typeof error === 'number' ? error : 1}`
)
console.warn(
`If you got an error related to FFMpeg or Protoc/Protobuf you may need to re-run \`pnpm prep\``
)
throw error
})
else if (process.platform === 'linux')
await spawn(path.join(__dirname, 'fix-deb.sh'), [], __dirname)
}
} catch (error) {
console.error(
`tauri ${args[0]} failed with exit code ${typeof error === 'number' ? error : 1}`
)
console.warn(
`If you got an error related to libav*/FFMpeg or Protoc/Protobuf you may need to re-run \`pnpm prep\``,
`If you got an error related to missing nasm you need to run ${
platform === 'win32' ? './scripts/setup.ps1' : './scripts/setup.sh'
}`
)
if (typeof error === 'number') {
code = error
} else {

View File

@@ -1,71 +1,25 @@
// Suffixes
export const PROTOC_SUFFIX = {
Linux: {
i386: 'linux-x86_32',
i686: 'linux-x86_32',
x86_64: 'linux-x86_64',
aarch64: 'linux-aarch_64',
},
Darwin: {
x86_64: 'osx-x86_64',
export const NATIVE_DEPS_URL =
'https://github.com/spacedriveapp/native-deps/releases/latest/download'
aarch64: 'osx-aarch_64',
},
Windows_NT: {
i386: 'win32',
i686: 'win32',
x86_64: 'win64',
},
}
export const PDFIUM_SUFFIX = {
export const NATIVE_DEPS_ASSETS = {
Linux: {
x86_64: {
musl: 'linux-musl-x64',
glibc: 'linux-x64',
},
aarch64: 'linux-arm64',
},
Darwin: {
x86_64: 'mac-x64',
aarch64: 'mac-arm64',
},
Windows_NT: {
x86_64: 'win-x64',
aarch64: 'win-arm64',
},
}
export const FFMPEG_SUFFFIX = {
Darwin: {
x86_64: 'x86_64',
aarch64: 'arm64',
},
Windows_NT: {
x86_64: 'x86_64',
},
}
export const FFMPEG_WORKFLOW = {
Darwin: 'ffmpeg-macos.yml',
Windows_NT: 'ffmpeg-windows.yml',
}
export const LIBHEIF_SUFFIX = {
Linux: {
x86_64: {
musl: 'x86_64-linux-musl',
glibc: 'x86_64-linux-gnu',
musl: 'native-deps-x86_64-linux-musl.tar.xz',
glibc: 'native-deps-x86_64-linux-gnu.tar.xz',
},
aarch64: {
musl: 'aarch64-linux-musl',
glibc: 'aarch64-linux-gnu',
musl: 'native-deps-aarch64-linux-musl.tar.xz',
glibc: 'native-deps-aarch64-linux-gnu.tar.xz',
},
},
}
export const LIBHEIF_WORKFLOW = {
Linux: 'libheif-linux.yml',
Darwin: {
x86_64: 'native-deps-x86_64-darwin-apple.tar.xz',
aarch64: 'native-deps-aarch64-darwin-apple.tar.xz',
},
Windows_NT: {
x86_64: 'native-deps-x86_64-windows-gnu.tar.xz ',
aarch64: 'native-deps-aarch64-windows-gnu.tar.xz',
},
}
/**
@@ -85,13 +39,3 @@ export function getConst(constants, identifiers) {
return typeof constant === 'string' ? constant : null
}
/**
* @param {Record<string, unknown>} suffixes
* @param {string[]} identifiers
* @returns {RegExp?}
*/
export function getSuffix(suffixes, identifiers) {
const suffix = getConst(suffixes, identifiers)
return suffix ? new RegExp(`${suffix}(\\.[^\\.]+)*$`) : null
}

View File

@@ -1,198 +0,0 @@
import * as fs from 'node:fs/promises'
import * as os from 'node:os'
import * as path from 'node:path'
import { env } from 'node:process'
import { extractTo } from 'archive-wasm/src/fs.mjs'
import {
FFMPEG_SUFFFIX,
FFMPEG_WORKFLOW,
getConst,
getSuffix,
LIBHEIF_SUFFIX,
LIBHEIF_WORKFLOW,
PDFIUM_SUFFIX,
PROTOC_SUFFIX,
} from './consts.mjs'
import {
getGh,
getGhArtifactContent,
getGhReleasesAssets,
getGhWorkflowRunArtifacts,
} from './github.mjs'
import { which } from './which.mjs'
const noop = () => {}
const __debug = env.NODE_ENV === 'debug'
const __osType = os.type()
// Github repos
const PDFIUM_REPO = 'bblanchon/pdfium-binaries'
const PROTOBUF_REPO = 'protocolbuffers/protobuf'
const SPACEDRIVE_REPO = 'spacedriveapp/spacedrive'
/**
* Download and extract protobuff compiler binary
* @param {string[]} machineId
* @param {string} nativeDeps
*/
export async function downloadProtc(machineId, nativeDeps) {
if (await which('protoc')) return
console.log('Downloading protoc...')
const protocSuffix = getSuffix(PROTOC_SUFFIX, machineId)
if (protocSuffix == null) throw new Error('NO_PROTOC')
let found = false
for await (const release of getGhReleasesAssets(PROTOBUF_REPO)) {
if (!protocSuffix.test(release.name)) continue
try {
await extractTo(await getGh(release.downloadUrl), nativeDeps, {
chmod: 0o600,
overwrite: true,
})
found = true
break
} catch (error) {
console.warn('Failed to download protoc, re-trying...')
if (__debug) console.error(error)
}
}
if (!found) throw new Error('NO_PROTOC')
// cleanup
await fs.unlink(path.join(nativeDeps, 'readme.txt')).catch(__debug ? console.error : noop)
}
/**
* Download and extract pdfium library for generating PDFs thumbnails
* @param {string[]} machineId
* @param {string} nativeDeps
*/
export async function downloadPDFium(machineId, nativeDeps) {
console.log('Downloading pdfium...')
const pdfiumSuffix = getSuffix(PDFIUM_SUFFIX, machineId)
if (pdfiumSuffix == null) throw new Error('NO_PDFIUM')
let found = false
for await (const release of getGhReleasesAssets(PDFIUM_REPO)) {
if (!pdfiumSuffix.test(release.name)) continue
try {
await extractTo(await getGh(release.downloadUrl), nativeDeps, {
chmod: 0o600,
overwrite: true,
})
found = true
break
} catch (error) {
console.warn('Failed to download pdfium, re-trying...')
if (__debug) console.error(error)
}
}
if (!found) throw new Error('NO_PDFIUM')
// cleanup
const cleanup = [
fs.rename(path.join(nativeDeps, 'LICENSE'), path.join(nativeDeps, 'LICENSE.pdfium')),
...['args.gn', 'PDFiumConfig.cmake', 'VERSION'].map(file =>
fs.unlink(path.join(nativeDeps, file)).catch(__debug ? console.error : noop)
),
]
switch (__osType) {
case 'Linux':
cleanup.push(fs.chmod(path.join(nativeDeps, 'lib', 'libpdfium.so'), 0o750))
break
case 'Darwin':
cleanup.push(fs.chmod(path.join(nativeDeps, 'lib', 'libpdfium.dylib'), 0o750))
break
}
await Promise.all(cleanup)
}
/**
* Download and extract ffmpeg libs for video thumbnails
* @param {string[]} machineId
* @param {string} nativeDeps
* @param {string[]} branches
*/
export async function downloadFFMpeg(machineId, nativeDeps, branches) {
const workflow = getConst(FFMPEG_WORKFLOW, machineId)
if (workflow == null) {
console.log('Checking FFMPeg...')
if (await which('ffmpeg')) {
// TODO: check ffmpeg version match what we need
return
} else {
throw new Error('NO_FFMPEG')
}
}
console.log('Downloading FFMPeg...')
const ffmpegSuffix = getSuffix(FFMPEG_SUFFFIX, machineId)
if (ffmpegSuffix == null) throw new Error('NO_FFMPEG')
let found = false
for await (const artifact of getGhWorkflowRunArtifacts(SPACEDRIVE_REPO, workflow, branches)) {
if (!ffmpegSuffix.test(artifact.name)) continue
try {
const data = await getGhArtifactContent(SPACEDRIVE_REPO, artifact.id)
await extractTo(data, nativeDeps, {
chmod: 0o600,
recursive: true,
overwrite: true,
})
found = true
break
} catch (error) {
console.warn('Failed to download FFMpeg, re-trying...')
if (__debug) console.error(error)
}
}
if (!found) throw new Error('NO_FFMPEG')
}
/**
* Download and extract libheif libs for heif thumbnails
* @param {string[]} machineId
* @param {string} nativeDeps
* @param {string[]} branches
*/
export async function downloadLibHeif(machineId, nativeDeps, branches) {
const workflow = getConst(LIBHEIF_WORKFLOW, machineId)
if (workflow == null) return
console.log('Downloading LibHeif...')
const libHeifSuffix = getSuffix(LIBHEIF_SUFFIX, machineId)
if (libHeifSuffix == null) throw new Error('NO_LIBHEIF')
let found = false
for await (const artifact of getGhWorkflowRunArtifacts(SPACEDRIVE_REPO, workflow, branches)) {
if (!libHeifSuffix.test(artifact.name)) continue
try {
const data = await getGhArtifactContent(SPACEDRIVE_REPO, artifact.id)
await extractTo(data, nativeDeps, {
chmod: 0o600,
recursive: true,
overwrite: true,
})
found = true
break
} catch (error) {
console.warn('Failed to download LibHeif, re-trying...')
if (__debug) console.error(error)
}
}
if (!found) throw new Error('NO_LIBHEIF')
}

138
scripts/utils/fetch.mjs Normal file
View File

@@ -0,0 +1,138 @@
import * as fs from 'node:fs/promises'
import { dirname, join as joinPath } from 'node:path'
import { env } from 'node:process'
import { fileURLToPath } from 'node:url'
import { fetch, Headers } from 'undici'
const __debug = env.NODE_ENV === 'debug'
const __offline = env.OFFLINE === 'true'
const __filename = fileURLToPath(import.meta.url)
const __dirname = dirname(__filename)
const cacheDir = joinPath(__dirname, '.tmp')
await fs.mkdir(cacheDir, { recursive: true, mode: 0o751 })
/**
* @param {string} resource
* @param {Headers} [headers]
* @returns {Promise<null | {data: Buffer, header: [string, string] | undefined}>}
*/
async function getCache(resource, headers) {
/** @type {Buffer | undefined} */
let data
/** @type {[string, string] | undefined} */
let header
// Don't cache in CI
if (env.CI === 'true') return null
if (headers)
resource += Array.from(headers.entries())
.filter(([name]) => name !== 'If-None-Match' && name !== 'If-Modified-Since')
.flat()
.join(':')
try {
const cache = JSON.parse(
await fs.readFile(joinPath(cacheDir, Buffer.from(resource).toString('base64url')), {
encoding: 'utf8',
})
)
if (cache && typeof cache === 'object') {
if (cache.etag && typeof cache.etag === 'string') {
header = ['If-None-Match', cache.etag]
} else if (cache.modifiedSince && typeof cache.modifiedSince === 'string') {
header = ['If-Modified-Since', cache.modifiedSince]
}
if (cache.data && typeof cache.data === 'string')
data = Buffer.from(cache.data, 'base64')
}
} catch (error) {
if (__debug) {
console.warn(`CACHE MISS: ${resource}`)
console.error(error)
}
}
return data ? { data, header } : null
}
/**
* @param {import('undici').Response} response
* @param {string} resource
* @param {Buffer} [cachedData]
* @param {Headers} [headers]
* @returns {Promise<Buffer>}
*/
async function setCache(response, resource, cachedData, headers) {
const data = Buffer.from(await response.arrayBuffer())
// Don't cache in CI
if (env.CI === 'true') return data
const etag = response.headers.get('ETag') || undefined
const modifiedSince = response.headers.get('Last-Modified') || undefined
if (headers)
resource += Array.from(headers.entries())
.filter(([name]) => name !== 'If-None-Match' && name !== 'If-Modified-Since')
.flat()
.join(':')
if (response.status === 304 || (response.ok && data.length === 0)) {
// Cache hit
if (!cachedData) throw new Error('Empty cache hit ????')
return cachedData
}
try {
await fs.writeFile(
joinPath(cacheDir, Buffer.from(resource).toString('base64url')),
JSON.stringify({
etag,
modifiedSince,
data: data.toString('base64'),
}),
{ mode: 0o640, flag: 'w+' }
)
} catch (error) {
if (__debug) {
console.warn(`CACHE WRITE FAIL: ${resource}`)
console.error(error)
}
}
return data
}
/**
* @param {URL | string} resource
* @param {Headers?} [headers]
* @param {boolean} [preferCache]
* @returns {Promise<Buffer>}
*/
export async function get(resource, headers, preferCache) {
if (headers == null) headers = new Headers()
if (resource instanceof URL) resource = resource.toString()
const cache = await getCache(resource, headers)
if (__offline) {
if (cache?.data == null)
throw new Error(`OFFLINE MODE: Cache for request ${resource} doesn't exist`)
return cache.data
}
if (preferCache && cache?.data != null) return cache.data
if (cache?.header) headers.append(...cache.header)
const response = await fetch(resource, { headers })
if (!response.ok) {
if (cache?.data) {
if (__debug) console.warn(`CACHE HIT due to fail: ${resource} ${response.statusText}`)
return cache.data
}
throw new Error(response.statusText)
}
return await setCache(response, resource, cache?.data, headers)
}

35
scripts/utils/flock.mjs Normal file
View File

@@ -0,0 +1,35 @@
import { exec as execCb } from 'node:child_process'
import { setTimeout } from 'node:timers/promises'
import { promisify } from 'node:util'
import { which } from './which.mjs'
const exec = promisify(execCb)
/**
* @param {string} file
* @returns {Promise<void>}
*/
export async function waitLockUnlock(file) {
if (!(await which('flock'))) throw new Error('flock is not installed')
let locked = false
while (!locked) {
try {
await exec(`flock -ns "${file}" -c true`)
await setTimeout(100)
} catch {
locked = true
}
}
while (locked) {
try {
await exec(`flock -ns "${file}" -c true`)
} catch {
await setTimeout(100)
continue
}
locked = false
}
}

View File

@@ -1,87 +0,0 @@
import { exec as execCb } from 'node:child_process'
import * as fs from 'node:fs/promises'
import * as path from 'node:path'
import { env } from 'node:process'
import { promisify } from 'node:util'
const __debug = env.NODE_ENV === 'debug'
const exec = promisify(execCb)
/**
* @param {string} repoPath
* @returns {Promise<string?>}
*/
async function getRemoteBranchName(repoPath) {
let branchName
try {
branchName = (await exec('git symbolic-ref --short HEAD', { cwd: repoPath })).stdout.trim()
if (!branchName) throw new Error('Empty local branch name')
} catch (error) {
if (__debug) {
console.warn(`Failed to read git local branch name`)
console.error(error)
}
return null
}
let remoteBranchName
try {
remoteBranchName = (
await exec(`git for-each-ref --format="%(upstream:short)" refs/heads/${branchName}`, {
cwd: repoPath,
})
).stdout.trim()
const [_, branch] = remoteBranchName.split('/')
if (!branch) throw new Error('Empty remote branch name')
remoteBranchName = branch
} catch (error) {
if (__debug) {
console.warn(`Failed to read git remote branch name`)
console.error(error)
}
return null
}
return remoteBranchName
}
// https://stackoverflow.com/q/3651860#answer-67151923
// eslint-disable-next-line no-control-regex
const REF_REGEX = /ref:\s+refs\/heads\/(?<branch>[^\s\x00-\x1F:?[\\^~]+)/
const GITHUB_REF_REGEX = /^refs\/heads\//
/**
* @param {string} repoPath
* @returns {Promise<string[]>}
*/
export async function getGitBranches(repoPath) {
const branches = ['main', 'master']
if (env.GITHUB_HEAD_REF) {
branches.unshift(env.GITHUB_HEAD_REF)
} else if (env.GITHUB_REF) {
branches.unshift(env.GITHUB_REF.replace(GITHUB_REF_REGEX, ''))
}
const remoteBranchName = await getRemoteBranchName(repoPath)
if (remoteBranchName) {
branches.unshift(remoteBranchName)
} else {
let head
try {
head = await fs.readFile(path.join(repoPath, '.git', 'HEAD'), { encoding: 'utf8' })
} catch (error) {
if (__debug) {
console.warn(`Failed to read git HEAD file`)
console.error(error)
}
return branches
}
const match = REF_REGEX.exec(head)
if (match?.groups?.branch) branches.unshift(match.groups.branch)
}
return branches
}

View File

@@ -1,386 +0,0 @@
import * as fs from 'node:fs/promises'
import { dirname, join as joinPath, posix as path } from 'node:path'
import { env } from 'node:process'
import { setTimeout } from 'node:timers/promises'
import { fileURLToPath } from 'node:url'
import { fetch, Headers } from 'undici'
const __debug = env.NODE_ENV === 'debug'
const __offline = env.OFFLINE === 'true'
const __filename = fileURLToPath(import.meta.url)
const __dirname = dirname(__filename)
const cacheDir = joinPath(__dirname, '.tmp')
await fs.mkdir(cacheDir, { recursive: true, mode: 0o751 })
// Note: Trailing slashs are important to correctly append paths
const GH = 'https://api.github.com/repos/'
const NIGTHLY = 'https://nightly.link/'
// Github routes
const RELEASES = 'releases'
const WORKFLOWS = 'actions/workflows'
const ARTIFACTS = 'actions/artifacts'
// Default GH headers
const GH_HEADERS = new Headers({
Accept: 'application/vnd.github+json',
'X-GitHub-Api-Version': '2022-11-28',
})
// Load github auth token if available
if ('GITHUB_TOKEN' in env && env.GITHUB_TOKEN)
GH_HEADERS.append('Authorization', `Bearer ${env.GITHUB_TOKEN}`)
/**
* @param {string} resource
* @param {Headers} [headers]
* @returns {Promise<null | {data: Buffer, header: [string, string] | undefined}>}
*/
async function getCache(resource, headers) {
/** @type {Buffer | undefined} */
let data
/** @type {[string, string] | undefined} */
let header
// Don't cache in CI
if (env.CI === 'true') return null
if (headers)
resource += Array.from(headers.entries())
.filter(([name]) => name !== 'If-None-Match' && name !== 'If-Modified-Since')
.flat()
.join(':')
try {
const cache = JSON.parse(
await fs.readFile(joinPath(cacheDir, Buffer.from(resource).toString('base64url')), {
encoding: 'utf8',
})
)
if (cache && typeof cache === 'object') {
if (cache.etag && typeof cache.etag === 'string') {
header = ['If-None-Match', cache.etag]
} else if (cache.modifiedSince && typeof cache.modifiedSince === 'string') {
header = ['If-Modified-Since', cache.modifiedSince]
}
if (cache.data && typeof cache.data === 'string')
data = Buffer.from(cache.data, 'base64')
}
} catch (error) {
if (__debug) {
console.warn(`CACHE MISS: ${resource}`)
console.error(error)
}
}
return data ? { data, header } : null
}
/**
* @param {import('undici').Response} response
* @param {string} resource
* @param {Buffer} [cachedData]
* @param {Headers} [headers]
* @returns {Promise<Buffer>}
*/
async function setCache(response, resource, cachedData, headers) {
const data = Buffer.from(await response.arrayBuffer())
// Don't cache in CI
if (env.CI === 'true') return data
const etag = response.headers.get('ETag') || undefined
const modifiedSince = response.headers.get('Last-Modified') || undefined
if (headers)
resource += Array.from(headers.entries())
.filter(([name]) => name !== 'If-None-Match' && name !== 'If-Modified-Since')
.flat()
.join(':')
if (response.status === 304 || (response.ok && data.length === 0)) {
// Cache hit
if (!cachedData) throw new Error('Empty cache hit ????')
return cachedData
}
try {
await fs.writeFile(
joinPath(cacheDir, Buffer.from(resource).toString('base64url')),
JSON.stringify({
etag,
modifiedSince,
data: data.toString('base64'),
}),
{ mode: 0o640, flag: 'w+' }
)
} catch (error) {
if (__debug) {
console.warn(`CACHE WRITE FAIL: ${resource}`)
console.error(error)
}
}
return data
}
/**
* @param {URL | string} resource
* @param {Headers?} [headers]
* @param {boolean} [preferCache]
* @returns {Promise<Buffer>}
*/
export async function get(resource, headers, preferCache) {
if (headers == null) headers = new Headers()
if (resource instanceof URL) resource = resource.toString()
const cache = await getCache(resource, headers)
if (__offline) {
if (cache?.data == null)
throw new Error(`OFFLINE MODE: Cache for request ${resource} doesn't exist`)
return cache.data
}
if (preferCache && cache?.data != null) return cache.data
if (cache?.header) headers.append(...cache.header)
const response = await fetch(resource, { headers })
if (!response.ok) {
if (cache?.data) {
if (__debug) console.warn(`CACHE HIT due to fail: ${resource} ${response.statusText}`)
return cache.data
}
throw new Error(response.statusText)
}
return await setCache(response, resource, cache?.data, headers)
}
// Header name Description
// x-ratelimit-limit The maximum number of requests you're permitted to make per hour.
// x-ratelimit-remaining The number of requests remaining in the current rate limit window.
// x-ratelimit-used The number of requests you've made in the current rate limit window.
// x-ratelimit-reset The time at which the current rate limit window resets in UTC epoch seconds.
const RATE_LIMIT = {
reset: 0,
remaining: Infinity,
}
/**
* Get resource from a Github route with some pre-defined parameters
* @param {string} route
* @returns {Promise<Buffer>}
*/
export async function getGh(route) {
route = new URL(route, GH).toString()
const cache = await getCache(route)
if (__offline) {
if (cache?.data == null)
throw new Error(`OFFLINE MODE: Cache for request ${route} doesn't exist`)
return cache?.data
}
if (RATE_LIMIT.remaining === 0) {
if (cache?.data) return cache.data
console.warn(
`RATE LIMIT: Waiting ${RATE_LIMIT.reset} seconds before contacting Github again... [CTRL+C to cancel]`
)
await setTimeout(RATE_LIMIT.reset * 1000)
}
const headers = new Headers(GH_HEADERS)
if (cache?.header) headers.append(...cache.header)
const response = await fetch(route, { method: 'GET', headers })
const rateReset = Number.parseInt(response.headers.get('x-ratelimit-reset') ?? '')
const rateRemaining = Number.parseInt(response.headers.get('x-ratelimit-remaining') ?? '')
if (!(Number.isNaN(rateReset) || Number.isNaN(rateRemaining))) {
const reset = rateReset - Date.now() / 1000
if (reset > RATE_LIMIT.reset) RATE_LIMIT.reset = reset
if (rateRemaining < RATE_LIMIT.remaining) {
RATE_LIMIT.remaining = rateRemaining
if (__debug) {
console.warn(`Github remaining requests: ${RATE_LIMIT.remaining}`)
await setTimeout(5000)
}
}
}
if (!response.ok) {
if (cache?.data) {
if (__debug) console.warn(`CACHE HIT due to fail: ${route} ${response.statusText}`)
return cache.data
}
if (response.status === 403 && RATE_LIMIT.remaining === 0) return await getGh(route)
throw new Error(response.statusText)
}
return await setCache(response, route, cache?.data)
}
/**
* @param {string} repo
* @yields {{name: string, downloadUrl: string}}
*/
export async function* getGhReleasesAssets(repo) {
let page = 0
while (true) {
// "${_gh_url}/protocolbuffers/protobuf/releases?page=${_page}&per_page=100"
const releases = JSON.parse(
(await getGh(path.join(repo, `${RELEASES}?page=${page++}&per_page=100`))).toString(
'utf8'
)
)
if (!Array.isArray(releases)) throw new Error(`Error: ${JSON.stringify(releases)}`)
if (releases.length === 0) return
for (const release of /** @type {unknown[]} */ (releases)) {
if (
!(
release &&
typeof release === 'object' &&
'assets' in release &&
Array.isArray(release.assets)
)
)
throw new Error(`Invalid release: ${release}`)
if ('prerelease' in release && release.prerelease) continue
for (const asset of /** @type {unknown[]} */ (release.assets)) {
if (
!(
asset &&
typeof asset === 'object' &&
'name' in asset &&
typeof asset.name === 'string' &&
'browser_download_url' in asset &&
typeof asset.browser_download_url === 'string'
)
)
throw new Error(`Invalid release.asset: ${asset}`)
yield { name: asset.name, downloadUrl: asset.browser_download_url }
}
}
}
}
/**
* @param {string} repo
* @param {string} yaml
* @param {string | Array.<string> | Set.<string>} [branch]
* @yields {{ id: number, name: string }}
*/
export async function* getGhWorkflowRunArtifacts(repo, yaml, branch) {
if (!branch) branch = 'main'
if (typeof branch === 'string') branch = [branch]
if (!(branch instanceof Set)) branch = new Set(branch)
let page = 0
while (true) {
const workflow = /** @type {unknown} */ (
JSON.parse(
(
await getGh(
path.join(
repo,
WORKFLOWS,
yaml,
`runs?page=${page++}&per_page=100&status=success`
)
)
).toString('utf8')
)
)
if (
!(
workflow &&
typeof workflow === 'object' &&
'workflow_runs' in workflow &&
Array.isArray(workflow.workflow_runs)
)
)
throw new Error(`Error: ${JSON.stringify(workflow)}`)
if (workflow.workflow_runs.length === 0) return
for (const run of /** @type {unknown[]} */ (workflow.workflow_runs)) {
if (
!(
run &&
typeof run === 'object' &&
'head_branch' in run &&
typeof run.head_branch === 'string' &&
'artifacts_url' in run &&
typeof run.artifacts_url === 'string'
)
)
throw new Error(`Invalid Workflow run: ${run}`)
if (!branch.has(run.head_branch)) continue
const response = /** @type {unknown} */ (
JSON.parse((await getGh(run.artifacts_url)).toString('utf8'))
)
if (
!(
response &&
typeof response === 'object' &&
'artifacts' in response &&
Array.isArray(response.artifacts)
)
)
throw new Error(`Error: ${JSON.stringify(response)}`)
for (const artifact of /** @type {unknown[]} */ (response.artifacts)) {
if (
!(
artifact &&
typeof artifact === 'object' &&
'id' in artifact &&
typeof artifact.id === 'number' &&
'name' in artifact &&
typeof artifact.name === 'string'
)
)
throw new Error(`Invalid artifact: ${artifact}`)
yield { id: artifact.id, name: artifact.name }
}
}
}
}
/**
* @param {string} repo
* @param {number} id
* @returns {Promise<Buffer>}
*/
export async function getGhArtifactContent(repo, id) {
// Artifacts can only be downloaded directly from Github with authorized requests
if (GH_HEADERS.has('Authorization')) {
try {
// "${_gh_url}/${_sd_gh_path}/actions/artifacts/${_artifact_id}/zip"
return await getGh(path.join(repo, ARTIFACTS, id.toString(), 'zip'))
} catch (error) {
if (__debug) {
console.warn('Failed to download artifact from github, fallback to nightly.link')
console.error(error)
}
}
}
/**
* nightly.link is a workaround for the lack of a public GitHub API to download artifacts from a workflow run
* https://github.com/actions/upload-artifact/issues/51
* Use it when running in evironments that are not authenticated with github
* "https://nightly.link/${_sd_gh_path}/actions/artifacts/${_artifact_id}.zip"
*/
return await get(new URL(path.join(repo, ARTIFACTS, `${id}.zip`), NIGTHLY), null, true)
}

View File

@@ -52,10 +52,12 @@ export async function tauriUpdaterKey(nativeDeps) {
/**
* @param {string} root
* @param {string} nativeDeps
* @param {string[]} targets
* @param {string[]} bundles
* @param {string[]} args
* @returns {Promise<string[]>}
*/
export async function patchTauri(root, nativeDeps, args) {
export async function patchTauri(root, nativeDeps, targets, bundles, args) {
if (args.findIndex(e => e === '-c' || e === '--config') !== -1) {
throw new Error('Custom tauri build config is not supported.')
}
@@ -66,7 +68,7 @@ export async function patchTauri(root, nativeDeps, args) {
const osType = os.type()
const resources =
osType === 'Linux'
? await copyLinuxLibs(root, nativeDeps)
? await copyLinuxLibs(root, nativeDeps, args[0] === 'dev')
: osType === 'Windows_NT'
? await copyWindowsDLLs(root, nativeDeps)
: { files: [], toClean: [] }
@@ -86,6 +88,12 @@ export async function patchTauri(root, nativeDeps, args) {
.readFile(path.join(tauriRoot, 'tauri.conf.json'), 'utf-8')
.then(JSON.parse)
if (bundles.length === 0) {
const defaultBundles = tauriConfig.tauri?.bundle?.targets
if (Array.isArray(defaultBundles)) bundles.push(...defaultBundles)
if (bundles.length === 0) bundles.push('all')
}
if (args[0] === 'build') {
if (tauriConfig?.tauri?.updater?.active) {
const pubKey = await tauriUpdaterKey(nativeDeps)
@@ -94,19 +102,10 @@ export async function patchTauri(root, nativeDeps, args) {
}
if (osType === 'Darwin') {
// ARM64 support was added in macOS 11, but we need at least 11.2 due to our ffmpeg build
const macOSArm64MinimumVersion = '11.2'
const macOSArm64MinimumVersion = '11.0'
let macOSMinimumVersion = tauriConfig?.tauri?.bundle?.macOS?.minimumSystemVersion
const targets = args
.filter((_, index, args) => {
if (index === 0) return false
const previous = args[index - 1]
return previous === '-t' || previous === '--target'
})
.flatMap(target => target.split(','))
if (
(targets.includes('aarch64-apple-darwin') ||
(targets.length === 0 && process.arch === 'arm64')) &&

View File

@@ -18,58 +18,6 @@ async function link(origin, target, rename) {
await (rename ? fs.rename(origin, target) : fs.symlink(path.relative(parent, origin), target))
}
/**
* Move headers and dylibs of external deps to our framework
* @param {string} nativeDeps
*/
export async function setupMacOsFramework(nativeDeps) {
// External deps
const lib = path.join(nativeDeps, 'lib')
const include = path.join(nativeDeps, 'include')
// Framework
const framework = path.join(nativeDeps, 'FFMpeg.framework')
const headers = path.join(framework, 'Headers')
const libraries = path.join(framework, 'Libraries')
const documentation = path.join(framework, 'Resources', 'English.lproj', 'Documentation')
// Move files
await Promise.all([
// Move pdfium license to framework
fs.rename(
path.join(nativeDeps, 'LICENSE.pdfium'),
path.join(documentation, 'LICENSE.pdfium')
),
// Move dylibs to framework
fs.readdir(lib, { recursive: true, withFileTypes: true }).then(file =>
file
.filter(
entry =>
(entry.isFile() || entry.isSymbolicLink()) && entry.name.endsWith('.dylib')
)
.map(entry => {
const file = path.join(entry.path, entry.name)
const newFile = path.resolve(libraries, path.relative(lib, file))
return link(file, newFile, true)
})
),
// Move headers to framework
fs.readdir(include, { recursive: true, withFileTypes: true }).then(file =>
file
.filter(
entry =>
(entry.isFile() || entry.isSymbolicLink()) &&
!entry.name.endsWith('.proto')
)
.map(entry => {
const file = path.join(entry.path, entry.name)
const newFile = path.resolve(headers, path.relative(include, file))
return link(file, newFile, true)
})
),
])
}
/**
* Symlink shared libs paths for Linux
* @param {string} root
@@ -87,56 +35,33 @@ export async function symlinkSharedLibsLinux(root, nativeDeps) {
/**
* Symlink shared libs paths for macOS
* @param {string} root
* @param {string} nativeDeps
*/
export async function symlinkSharedLibsMacOS(nativeDeps) {
// External deps
const lib = path.join(nativeDeps, 'lib')
const include = path.join(nativeDeps, 'include')
export async function symlinkSharedLibsMacOS(root, nativeDeps) {
// rpath=@executable_path/../Frameworks/Spacedrive.framework
const targetFrameworks = path.join(root, 'target', 'Frameworks')
// Framework
const framework = path.join(nativeDeps, 'FFMpeg.framework')
const headers = path.join(framework, 'Headers')
const libraries = path.join(framework, 'Libraries')
const framework = path.join(nativeDeps, 'Spacedrive.framework')
// Link files
await Promise.all([
// Link header files
fs.readdir(headers, { recursive: true, withFileTypes: true }).then(files =>
// Link Spacedrive.framework to target folder so sd-server can work ootb
await fs.rm(targetFrameworks, { recursive: true }).catch(() => {})
await fs.mkdir(targetFrameworks, { recursive: true })
await link(framework, path.join(targetFrameworks, 'Spacedrive.framework'))
// Sign dylibs (Required for them to work on macOS 13+)
await fs
.readdir(path.join(framework, 'Libraries'), { recursive: true, withFileTypes: true })
.then(files =>
Promise.all(
files
.filter(entry => entry.isFile() || entry.isSymbolicLink())
.map(entry => {
const file = path.join(entry.path, entry.name)
return link(file, path.resolve(include, path.relative(headers, file)))
})
)
),
// Link dylibs
fs.readdir(libraries, { recursive: true, withFileTypes: true }).then(files =>
Promise.all(
files
.filter(
entry =>
(entry.isFile() || entry.isSymbolicLink()) &&
entry.name.endsWith('.dylib')
.filter(entry => entry.isFile() && entry.name.endsWith('.dylib'))
.map(entry =>
exec(`codesign -s "${signId}" -f "${path.join(entry.path, entry.name)}"`)
)
.map(entry => {
const file = path.join(entry.path, entry.name)
/** @type {Promise<unknown>[]} */
const actions = [
link(file, path.resolve(lib, path.relative(libraries, file))),
]
// Sign dylib (Required for it to work on macOS 13+)
if (entry.isFile())
actions.push(exec(`codesign -s "${signId}" -f "${file}"`))
return actions.length > 1 ? Promise.all(actions) : actions[0]
})
)
),
])
)
}
/**
@@ -168,9 +93,10 @@ export async function copyWindowsDLLs(root, nativeDeps) {
* Symlink shared libs paths for Linux
* @param {string} root
* @param {string} nativeDeps
* @param {boolean} isDev
* @returns {Promise<{files: string[], toClean: string[]}>}
*/
export async function copyLinuxLibs(root, nativeDeps) {
export async function copyLinuxLibs(root, nativeDeps, isDev) {
// rpath=${ORIGIN}/../lib/spacedrive
const tauriSrc = path.join(root, 'apps', 'desktop', 'src-tauri')
const files = await fs
@@ -184,10 +110,17 @@ export async function copyLinuxLibs(root, nativeDeps) {
(entry.name.endsWith('.so') || entry.name.includes('.so.'))
)
.map(async entry => {
await fs.copyFile(
path.join(entry.path, entry.name),
path.join(tauriSrc, entry.name)
)
if (entry.isSymbolicLink()) {
await fs.symlink(
await fs.readlink(path.join(entry.path, entry.name)),
path.join(tauriSrc, entry.name)
)
} else {
const target = path.join(tauriSrc, entry.name)
await fs.copyFile(path.join(entry.path, entry.name), target)
// https://web.archive.org/web/20220731055320/https://lintian.debian.org/tags/shared-library-is-executable
await fs.chmod(target, 0o644)
}
return entry.name
})
)
@@ -195,6 +128,9 @@ export async function copyLinuxLibs(root, nativeDeps) {
return {
files,
toClean: files.map(file => path.join(tauriSrc, file)),
toClean: [
...files.map(file => path.join(tauriSrc, file)),
...files.map(file => path.join(root, 'target', isDev ? 'debug' : 'release', file)),
],
}
}