mirror of
https://github.com/spacedriveapp/spacedrive.git
synced 2026-05-18 13:26:00 -04:00
[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:
committed by
GitHub
parent
c2dd3661f9
commit
48afea5a4b
172
scripts/fix-deb.sh
Executable file
172
scripts/fix-deb.sh
Executable 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
69
scripts/preprep.mjs
Normal file → Executable 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
|
||||
})
|
||||
|
||||
@@ -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
119
scripts/tauri.mjs
Normal file → Executable 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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
138
scripts/utils/fetch.mjs
Normal 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
35
scripts/utils/flock.mjs
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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')) &&
|
||||
|
||||
@@ -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)),
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user