[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

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)),
],
}
}