Merge branch 'main' into release-build-fixes

This commit is contained in:
Jamie Pine
2026-02-06 19:20:52 -08:00
20 changed files with 877 additions and 7298 deletions

View File

@@ -143,3 +143,13 @@ lto = true # Enables link to optimizations
opt-level = "s" # Optimize for binary size
panic = "unwind" # Sadly we need unwind to avoid unexpected crashes on third party crates
strip = true # Remove debug symbols
# Fast mobile development profile - trades binary size for build speed
# Use with: cargo build --profile mobile-dev
# For production mobile releases, use --release instead
[profile.mobile-dev]
inherits = "release"
codegen-units = 16 # Allow parallel compilation (vs 1 in release)
lto = false # Disable link-time optimization (saves 15-20 min per arch)
opt-level = 2 # Good optimization without extreme size focus
strip = true # Still remove debug symbols

124
README.md
View File

@@ -127,26 +127,66 @@ Peer-to-peer synchronization without central coordinators. Device-specific data
**Core**
- **Rust** - Entire VDFS implementation (~183k lines)
- **SQLite + SeaORM** - Local-first database with type-safe queries
- **Iroh** - P2P networking with QUIC transport and hole-punching
- **Tokio** - Async runtime
- **SQLite + SeaORM** - Local-first database with type-safe ORM queries
- **Iroh** - P2P networking with QUIC transport, hole-punching, and local discovery
- **BLAKE3** - Fast cryptographic hashing for content identity
- **WASM** - Sandboxed extension runtime
- **Wasmer** - Sandboxed WASM extension runtime
- **Axum** - HTTP/GraphQL server for web and API access
- **OpenDAL** - Unified cloud storage abstraction (S3, Google Drive, OneDrive, Dropbox, Azure Blob, GCS)
- **Specta** - Auto-generated TypeScript and Swift types from Rust
**Apps**
**Cryptography & Security**
- **CLI** - Command-line interface (available now)
- **Server** - Headless daemon for Docker deployment ([self-hosting guide](https://v2.spacedrive.com/overview/self-hosting))
- **Tauri** - Cross-platform desktop with React frontend (macOS and Linux now, Windows in alpha.2)
- **Web** - Web interface and shared UI components (available now)
- **Mobile** - React Native mobile app (iOS and Android coming soon)
- **Prototypes** - Native Swift apps (iOS, macOS) and GPUI media viewer for exploration
- **Ed25519 / X25519** - Signatures and key exchange
- **ChaCha20-Poly1305 / AES-GCM** - Authenticated encryption
- **Argon2** - Password hashing
- **BIP39** - Mnemonic phrase support for key backup
- **redb** - Encrypted key-value store for credentials
**Media Processing**
- **FFmpeg** (via custom `sd-ffmpeg` crate) - Video thumbnails, audio extraction
- **libheif** - HEIF/HEIC image support
- **Pdfium** - PDF rendering
- **Whisper** - On-device speech recognition (Metal-accelerated on Apple platforms)
- **Blurhash** - Compact image placeholders
**Interface** (shared across web and desktop)
- **React 19** - UI framework
- **Vite** - Build tooling
- **TypeScript** - Type-safe frontend code
- **TanStack Query** - Server state management
- **Zustand** - Client state management
- **Radix UI** - Accessible headless components
- **Tailwind CSS** - Utility-first styling
- **Framer Motion** - Animations
- **React Hook Form + Zod** - Form management and validation
- **Three.js / React Three Fiber** - 3D visualization
- **dnd-kit** - Drag and drop
- **TanStack Virtual / TanStack Table** - Virtualized lists and tables
**Desktop**
- **Tauri 2** - Cross-platform desktop shell (macOS, Linux, Windows)
**Mobile (React Native)**
- **React Native** 0.81 + **Expo** - Cross-platform mobile framework
- **Expo Router** - File-based routing
- **NativeWind** - Tailwind CSS for React Native
- **React Navigation** - Native navigation stack
- **Reanimated** - Native-thread animations
- **sd-mobile-core** - Rust core bridge via FFI
**Architecture Patterns**
- Event-driven design with centralized EventBus
- CQRS: Actions (mutations) and Queries (reads) with preview-commit-verify
- Durable jobs with MessagePack serialization
- Durable jobs with MessagePack serialization and checkpointing
- Domain-separated sync with clear data ownership boundaries
- Compile-time operation registration via `inventory` crate
---
@@ -154,25 +194,49 @@ Peer-to-peer synchronization without central coordinators. Device-specific data
```
spacedrive/
├── core/ # Rust VDFS implementation
├── core/ # Rust VDFS implementation
│ ├── src/
│ │ ├── domain/ # Core models (Entry, Library, Device, Tag)
│ │ ├── ops/ # CQRS operations (actions & queries)
│ │ ├── infra/ # Infrastructure (DB, events, jobs, sync)
│ │ ├── service/ # High-level services (network, file sharing)
│ │ ├── location/ # Location management and indexing
│ │ ├── library/ # Library lifecycle and operations
│ │ ── volume/ # Volume detection and fingerprinting
│ │ ├── domain/ # Core models (Entry, Library, Device, Tag, Volume)
│ │ ├── ops/ # CQRS operations (actions & queries)
│ │ ├── infra/ # Infrastructure (DB, events, jobs, sync)
│ │ ├── service/ # High-level services (network, file sharing, sync)
│ │ ├── crypto/ # Key management and encryption
│ │ ├── device/ # Device identity and configuration
│ │ ── filetype/ # File type detection and registry
│ │ ├── location/ # Location management and indexing
│ │ ├── library/ # Library lifecycle and operations
│ │ └── volume/ # Volume detection and fingerprinting
│ └── tests/ # Integration tests (pairing, sync, file transfer)
├── apps/
│ ├── cli/ # CLI for managing libraries and running daemon
│ ├── server/ # Headless server daemon
│ ├── tauri/ # Cross-platform desktop app (macOS, Windows, Linux)
│ ├── ios/ # Native prototype (private)
│ ├── macos/ # Native prototype (private)
── gpui-photo-grid/ # GPUI media viewer prototype
├── extensions/ # WASM extensions
├── crates/ # Shared Rust utilities
└── docs/ # Architecture documentation
│ ├── cli/ # CLI and daemon entry point
│ ├── server/ # Headless server for Docker/self-hosting
│ ├── tauri/ # Desktop app shell (macOS, Windows, Linux)
│ ├── web/ # Web app (Vite, connects to daemon via WebSocket)
│ ├── mobile/ # React Native mobile app (Expo)
── api/ # Cloud API server (Bun + Elysia)
│ ├── landing/ # Marketing site and docs (Next.js)
│ ├── ios/ # Native iOS prototype (Swift)
│ ├── macos/ # Native macOS prototype (Swift)
│ └── gpui-photo-grid/ # GPUI media viewer prototype
├── packages/
│ ├── interface/ # Shared React UI (used by web and desktop)
│ ├── ts-client/ # Auto-generated TypeScript client and hooks
│ ├── swift-client/ # Auto-generated Swift client
│ ├── ui/ # Shared component library
│ └── assets/ # Icons and images
├── crates/
│ ├── crypto/ # Cryptographic primitives
│ ├── ffmpeg/ # FFmpeg bindings for video/audio
│ ├── images/ # Image processing (HEIF, PDF, SVG)
│ ├── media-metadata/ # EXIF/media metadata extraction
│ ├── fs-watcher/ # Cross-platform file system watcher
│ ├── sdk/ # WASM extension SDK
│ ├── sdk-macros/ # Extension procedural macros
│ ├── task-system/ # Durable job execution engine
│ ├── sd-client/ # Rust client library
│ └── ... # actors, fda, log-analyzer, utils
├── extensions/ # WASM extensions (photos, test-extension)
└── docs/ # Architecture documentation
```
---
@@ -262,10 +326,6 @@ cargo run -p sd-cli -- search .
# Now launch Tauri app - it will connect to the running daemon
```
### Native Prototypes
Experimental native apps are available in `apps/ios/`, `apps/macos/`, and `apps/gpui-photo-grid/` but are not documented for public use. These prototypes explore platform-specific optimizations and alternative UI approaches.
### Running Tests
Spacedrive has a comprehensive test suite covering single-device operations and multi-device networking scenarios.

View File

@@ -7,8 +7,12 @@ const workspaceRoot = path.resolve(projectRoot, "../..");
const config = getDefaultConfig(projectRoot);
// Watch entire monorepo for hot reload
config.watchFolders = [workspaceRoot];
// Watch only relevant directories for hot reload (not entire monorepo)
// This avoids watching Rust target/ dirs (4.5GB+) and other build artifacts
config.watchFolders = [
path.resolve(projectRoot, "src"),
path.resolve(workspaceRoot, "packages"),
];
// Configure resolver for monorepo and SVG support
config.resolver = {
@@ -25,17 +29,18 @@ config.resolver = {
path.resolve(workspaceRoot, "node_modules"),
],
// Exclude build outputs and prevent loading wrong React version from root
// Exclude build outputs
blockList: [
/\/apps\/mobile\/ios\/build\/.*/,
/\/apps\/mobile\/android\/build\/.*/,
// Block React from workspace root to force local version
new RegExp(`^${workspaceRoot}/node_modules/react/.*`),
],
// Force React resolution from mobile app's node_modules
// Dynamically resolve React/React Native from wherever the package manager installed them
extraNodeModules: {
react: path.resolve(projectRoot, "node_modules/react"),
react: path.dirname(require.resolve("react/package.json", { paths: [projectRoot, workspaceRoot] })),
"react-native": path.dirname(
require.resolve("react-native/package.json", { paths: [projectRoot, workspaceRoot] })
),
},
};

View File

@@ -17,10 +17,14 @@ pwd
export CFLAGS_aarch64_apple_ios="-fno-stack-check -fno-stack-protector"
export CFLAGS_aarch64_apple_ios_sim="-fno-stack-check -fno-stack-protector"
# Clean aws-lc-sys build cache to avoid stale cmake state
echo "Cleaning aws-lc-sys build cache..."
rm -rf apps/mobile/modules/sd-mobile-core/core/target/aarch64-apple-ios/release/build/aws-lc-sys-* || true
rm -rf apps/mobile/modules/sd-mobile-core/core/target/aarch64-apple-ios-sim/release/build/aws-lc-sys-* || true
# Clean aws-lc-sys build cache if requested (fixes stale cmake state when
# switching between device/simulator or after Xcode updates)
# Usage: export CLEAN_AWS_LC=1 before building in Xcode, or: CLEAN_AWS_LC=1 bun run ios
if [ "${CLEAN_AWS_LC:-0}" = "1" ]; then
echo "Cleaning aws-lc-sys build cache..."
rm -rf target/aarch64-apple-ios/release/build/aws-lc-sys-* || true
rm -rf target/aarch64-apple-ios-sim/release/build/aws-lc-sys-* || true
fi
# Run xtask to build mobile libraries
cargo xtask build-mobile

View File

File diff suppressed because it is too large Load Diff

View File

@@ -67,7 +67,6 @@
"@babel/core": "^7.26.0",
"@babel/plugin-transform-runtime": "^7.28.5",
"@babel/runtime": "^7.28.4",
"@types/react": "~19.1.10",
"babel-preset-expo": "~54.0.0",
"eslint": "^9.15.0",
"prettier": "^3.3.3",

View File

@@ -181,9 +181,9 @@ impl LocationManager {
accessed_at: Set(None),
indexed_at: Set(Some(now)), // Record when location root was created
permissions: Set(None),
inode: Set(inode.map(|i| i as i64)),
parent_id: Set(None), // Location root has no parent
volume_id: Set(Some(volume_id)), // Volume is required for all locations
inode: Set(inode.map(|i| i as i64)), // Use extracted inode
parent_id: Set(None), // Location root has no parent
volume_id: Set(Some(volume_id)), // Volume is required for all locations
..Default::default()
};

View File

@@ -6,6 +6,8 @@
"typecheck": "bun run --filter @sd/tauri typecheck"
},
"devDependencies": {
"@types/react": "~19.1.10",
"@types/react-dom": "~19.1.10",
"@babel/plugin-syntax-import-assertions": "^7.24.0",
"@cspell/dict-rust": "^4.0.2",
"@cspell/dict-typescript": "^3.1.2",
@@ -30,6 +32,8 @@
"packageManager": "bun@1.3.0",
"overrides": {
"@types/node": ">18.18.x",
"@types/react": "~19.1.10",
"@types/react-dom": "~19.1.10",
"react": "19.1.0",
"react-dom": "19.1.0",
"react-router": "=6.20.1",

View File

@@ -6,6 +6,19 @@
"name": "@spacedriveapp/assets"
},
"sideEffects": false,
"types": "./types.d.ts",
"exports": {
"./icons": "./icons/index.ts",
"./icons/*": "./icons/*",
"./images": "./images/index.ts",
"./images/*": "./images/*",
"./svgs/*": "./svgs/*",
"./sounds": "./sounds/index.ts",
"./sounds/*": "./sounds/*",
"./videos/*": "./videos/*",
"./lottie/*": "./lottie/*",
"./util": "./util/index.ts"
},
"files": [
"icons",
"images",
@@ -13,7 +26,8 @@
"sounds",
"svgs",
"videos",
"util"
"util",
"types.d.ts"
],
"scripts": {
"gen": "node ./scripts/generate.mjs"

42
packages/assets/types.d.ts vendored Normal file
View File

@@ -0,0 +1,42 @@
// Type declarations for @sd/assets
declare module "@sd/assets/icons/*.png" {
const value: number; // React Native uses numeric IDs for local images
export default value;
}
declare module "@sd/assets/icons/*.jpg" {
const value: number;
export default value;
}
declare module "@sd/assets/images/*.png" {
const value: number;
export default value;
}
declare module "@sd/assets/images/*.jpg" {
const value: number;
export default value;
}
declare module "@sd/assets/svgs/*.svg" {
import type { FC } from "react";
const content: FC<Record<string, unknown>>;
export default content;
}
declare module "@sd/assets/videos/*.mp4" {
const value: number;
export default value;
}
declare module "@sd/assets/sounds/*.mp3" {
const value: number | string; // number on React Native (asset ID), string on web (URL)
export default value;
}
declare module "@sd/assets/lottie/*.json" {
const value: object;
export default value;
}

View File

Binary file not shown.

View File

@@ -3,6 +3,7 @@ import { Ball } from "@sd/assets/images";
import Orb from "../../components/Orb";
import { TopBarButton } from "@sd/ui";
import { GlobeHemisphereWest, GithubLogo, DiscordLogo } from "@phosphor-icons/react";
import contributors from "../../contributors.json";
export function AboutSettings() {
@@ -96,11 +97,36 @@ export function AboutSettings() {
</a>
</motion.div>
{/* Contributors */}
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: 0.45 }}
className="max-w-lg text-center mb-8 px-4"
>
<p className="text-[11px] leading-relaxed text-white/30">
{contributors.map((c: { name: string; github: string }, i) => (
<span key={c.github}>
{i > 0 && " · "}
<a
href={`https://github.com/${c.github}`}
target="_blank"
rel="noopener noreferrer"
title={`@${c.github}`}
className="hover:text-white/50 transition-colors"
>
{c.name}
</a>
</span>
))}
</p>
</motion.div>
{/* License */}
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.5, delay: 0.5 }}
transition={{ duration: 0.5, delay: 0.55 }}
className="text-center"
>
<a

View File

@@ -0,0 +1,450 @@
[
{
"name": "Jamie Pine",
"github": "jamiepine"
},
{
"name": "Brendan Allan",
"github": "Brendonovich"
},
{
"name": "Oscar Beaumont",
"github": "oscartbeaumont"
},
{
"name": "Ameer Al Ashhab",
"github": "ameer2468"
},
{
"name": "Arnab Chakraborty",
"github": "Rocky43007"
},
{
"name": "Ericson \"Fogo\" Soares",
"github": "fogodev"
},
{
"name": "nikec",
"github": "niikeec"
},
{
"name": "Vítor Vasconcellos",
"github": "HeavenVolkoff"
},
{
"name": "maxichrome",
"github": "maxichrome"
},
{
"name": "jake",
"github": "brxken128"
},
{
"name": "Utku",
"github": "utkubakir"
},
{
"name": "Lynx",
"github": "iLynxcat"
},
{
"name": "Matthew Yung",
"github": "myung03"
},
{
"name": "Julian Braha",
"github": "julianbraha"
},
{
"name": "Gedeon",
"github": "gedeondoescode"
},
{
"name": "William Stoneham",
"github": "RockBacon9922"
},
{
"name": "Haris",
"github": "xPolar"
},
{
"name": "Artsiom Voitas",
"github": "artsiom-voitas"
},
{
"name": "Benjamin",
"github": "benja"
},
{
"name": "Consoli",
"github": "matheus-consoli"
},
{
"name": "Andre",
"github": "CreatingBytes"
},
{
"name": "Aditya",
"github": "Raghav-45"
},
{
"name": "pr",
"github": "PineappleRind"
},
{
"name": "Stella",
"github": "KodingDev"
},
{
"name": "voletro",
"github": "voletro"
},
{
"name": "Lars Gyrup Brink Nielsen",
"github": "LayZeeDK"
},
{
"name": "Olen Latham",
"github": "PyRo1121"
},
{
"name": "Starbird",
"github": "StarbirdTech"
},
{
"name": "Harry Hopkinson",
"github": "Harry-Hopkinson"
},
{
"name": "Abhinav A",
"github": "abhnva"
},
{
"name": "wany-oh",
"github": "wany-oh"
},
{
"name": "Zakher Masri",
"github": "zaaakher"
},
{
"name": "Yukeey",
"github": "Yukeey07"
},
{
"name": "Md. Fahim Bin Amin",
"github": "FahimFBA"
},
{
"name": "Baran Mordoğan",
"github": "okunamayanad"
},
{
"name": "Na Risong",
"github": "HeavySnowJakarta"
},
{
"name": "Sreecharan",
"github": "sr2echa"
},
{
"name": "matt morris",
"github": "mmattbtw"
},
{
"name": "Jules Guesnon",
"github": "JulesGuesnon"
},
{
"name": "Madison Konig",
"github": "MadisonKonig"
},
{
"name": "Omar Hamad",
"github": "etahamad"
},
{
"name": "he1d1",
"github": "he1d1"
},
{
"name": "Ned Park",
"github": "ned-park"
},
{
"name": "Carter",
"github": "berbaroovez"
},
{
"name": "Tom Heaton",
"github": "tomheaton"
},
{
"name": "Tim Havlicek",
"github": "luckydye"
},
{
"name": "Tilo",
"github": "Tilo-K"
},
{
"name": "Sinan Gençoğlu",
"github": "SinanGncgl"
},
{
"name": "Michelangelo Guerra",
"github": "XSPGMike"
},
{
"name": "Marques Scripps",
"github": "MarquesCoding"
},
{
"name": "Abe",
"github": "FastestMolasses"
},
{
"name": "Carter",
"github": "Carterpersall"
},
{
"name": "Vikram Srinivas",
"github": "vikram2009"
},
{
"name": "Twan L",
"github": "TwanLuttik"
},
{
"name": "Syntax",
"github": "TheUltDev"
},
{
"name": "Takshil Mistry",
"github": "daUnknownCoder"
},
{
"name": "Sarah Bobbe",
"github": "SBobbe"
},
{
"name": "Ramprakash",
"github": "CodePurble"
},
{
"name": "Johan Sandgren",
"github": "Qrutz"
},
{
"name": "Phedona",
"github": "Phedona"
},
{
"name": "Percy Ma",
"github": "kecrily"
},
{
"name": "Param Birje",
"github": "ParamBirje"
},
{
"name": "Niklas Wojtkowiak",
"github": "0xnim"
},
{
"name": "Nicolás Fishman",
"github": "nicofishman"
},
{
"name": "Mykola",
"github": "handicraftsman"
},
{
"name": "Nicholas",
"github": "alsonick"
},
{
"name": "0xBA5E64",
"github": "0xBA5E64"
},
{
"name": "Whisht",
"github": "Whisht"
},
{
"name": "Yousef Abu-Salah",
"github": "ykabusalah"
},
{
"name": "andriizaiets",
"github": "andriizaiets"
},
{
"name": "devqore",
"github": "devqore"
},
{
"name": "erikpodusel",
"github": "erikpodusel"
},
{
"name": "David",
"github": "fivestones"
},
{
"name": "Christopher",
"github": "itschip"
},
{
"name": "jenniferdewan",
"github": "jenniferdewan"
},
{
"name": "leo",
"github": "greendoescode"
},
{
"name": "lzt1008",
"github": "lzt1008"
},
{
"name": "mark-strudwick",
"github": "mark-strudwick"
},
{
"name": "markrieder",
"github": "markrieder"
},
{
"name": "mooy",
"github": "mooyg"
},
{
"name": "S.L",
"github": "slvnlrt"
},
{
"name": "AhmedKaram",
"github": "Adamkaram"
},
{
"name": "Alex",
"github": "misxki"
},
{
"name": "Allie",
"github": "ChildishGiant"
},
{
"name": "Anthony Morris",
"github": "amorriscode"
},
{
"name": "BI3TKL",
"github": "Nightingale0504"
},
{
"name": "briamoe",
"github": "briamoe"
},
{
"name": "Bryan",
"github": "CodeWithBryan"
},
{
"name": "Cavell Blood",
"github": "cavellblood"
},
{
"name": "Cedric ",
"github": "ceddy4395"
},
{
"name": "Christo Todorov",
"github": "chroxify"
},
{
"name": "Colin Griffin",
"github": "krumware"
},
{
"name": "Conrad Crawford",
"github": "cnrad"
},
{
"name": "Eric Wyne",
"github": "ecwyne"
},
{
"name": "HardikBandhiya",
"github": "BandhiyaHardik"
},
{
"name": "Ilkka Poutanen",
"github": "ilkka"
},
{
"name": "Hesham Abourgheba",
"github": "IllusionMan1212"
},
{
"name": "Jeremy Möglich",
"github": "JeremyMoeglich"
},
{
"name": "Jesse Rodrigo",
"github": "JSSRDRG"
},
{
"name": "John Xu",
"github": "dyxushuai"
},
{
"name": "Jx",
"github": "JxJxxJxJ"
},
{
"name": "erian",
"github": "oopserian"
},
{
"name": "Leora",
"github": "Kuuchuu"
},
{
"name": "Liam Brewer",
"github": "liambrewer"
},
{
"name": "Lkhsss",
"github": "Lkhsss"
},
{
"name": "Majal",
"github": "majal"
},
{
"name": "Marc Espin",
"github": "marc2332"
},
{
"name": "Matteo Galiazzo",
"github": "gekoxyz"
},
{
"name": "Matthias Berchtold",
"github": "iammatthi"
},
{
"name": "Mohammed Bajuaifer",
"github": "MohammedBajuaifer"
},
{
"name": "Naman Garg",
"github": "namanlp"
},
{
"name": "Nebhay",
"github": "Nebhay"
}
]

View File

@@ -8,6 +8,7 @@
"module": "ESNext",
"target": "ES2022",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"baseUrl": ".",
"paths": {
"~/*": ["./src/*"]

View File

File diff suppressed because it is too large Load Diff

View File

@@ -13,9 +13,29 @@
"types": "./src/generated/types.ts",
"default": "./src/generated/types.ts"
},
"./src/generated/types": {
"types": "./src/generated/types.ts",
"default": "./src/generated/types.ts"
},
"./hooks": {
"types": "./src/hooks/index.ts",
"default": "./src/hooks/index.ts"
},
"./hooks/useClient": {
"types": "./src/hooks/useClient.tsx",
"default": "./src/hooks/useClient.tsx"
},
"./hooks/useNormalizedQuery": {
"types": "./src/hooks/useNormalizedQuery.ts",
"default": "./src/hooks/useNormalizedQuery.ts"
},
"./src/hooks/useClient": {
"types": "./src/hooks/useClient.tsx",
"default": "./src/hooks/useClient.tsx"
},
"./src/hooks/useNormalizedQuery": {
"types": "./src/hooks/useNormalizedQuery.ts",
"default": "./src/hooks/useNormalizedQuery.ts"
}
},
"scripts": {
@@ -46,12 +66,10 @@
},
"devDependencies": {
"@happy-dom/global-registrator": "^15.11.0",
"@tanstack/react-query": "^5.62.0",
"@testing-library/jest-dom": "^6.0.0",
"@testing-library/react": "^16.0.0",
"@types/jest": "^29.0.0",
"@types/node": "^20.0.0",
"@types/react": "^19.0.0",
"happy-dom": "^15.11.0",
"jest": "^29.0.0",
"jest-environment-jsdom": "^29.0.0",
@@ -64,8 +82,6 @@
"@sd/assets": "workspace:*",
"ws": "^8.0.0",
"zustand": "^5.0.8",
"react": "^19.0.0",
"@tanstack/react-query": "^5.62.0",
"ts-deepmerge": "^7.0.1",
"tiny-invariant": "^1.3.3",
"valibot": "^1.0.0",
@@ -75,4 +91,4 @@
"dist/**/*",
"src/**/*"
]
}
}

View File

@@ -9,7 +9,7 @@ anyhow = "1"
flate2 = "1.0"
mustache = "0.9"
owo-colors = "4"
reqwest = { version = "0.12", features = ["blocking", "rustls-tls"], default-features = false }
reqwest = { version = "0.12", features = ["blocking", "json", "rustls-tls"], default-features = false }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
tar = "0.4"

View File

@@ -120,16 +120,13 @@ pub fn generate_cargo_config(
protoc,
mobile_native_deps,
android_ndk_home,
// Android NDK host tag - the prebuilt directory is always named darwin-x86_64 on macOS,
// but the binaries are universal (fat) binaries with native ARM64 support.
// Google kept the path name for backwards compatibility.
host_tag: match system.os {
Os::Windows => "windows-x86_64",
Os::Linux => "linux-x86_64",
Os::MacOS => {
if cfg!(target_arch = "aarch64") {
"darwin-aarch64"
} else {
"darwin-x86_64"
}
}
Os::MacOS => "darwin-x86_64",
},
is_win: matches!(system.os, Os::Windows),
is_macos: matches!(system.os, Os::MacOS),

159
xtask/src/contributors.rs Normal file
View File

@@ -0,0 +1,159 @@
use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
use std::path::Path;
const REPO: &str = "spacedriveapp/spacedrive";
const OUTPUT_PATH: &str = "packages/interface/src/contributors.json";
const EXCLUDED_LOGINS: &[&str] = &["cursoragent"];
#[derive(Deserialize)]
struct GitHubContributor {
login: String,
#[serde(rename = "type")]
account_type: String,
}
#[derive(Deserialize)]
struct GitHubUser {
name: Option<String>,
}
#[derive(Serialize)]
struct Contributor {
name: String,
github: String,
}
/// Try to get a GitHub token from the environment or `gh` CLI
fn get_github_token() -> Option<String> {
if let Ok(token) = std::env::var("GITHUB_TOKEN") {
return Some(token);
}
std::process::Command::new("gh")
.args(["auth", "token"])
.output()
.ok()
.and_then(|o| {
if o.status.success() {
String::from_utf8(o.stdout).ok().map(|s| s.trim().to_string())
} else {
None
}
})
}
fn github_get(
client: &reqwest::blocking::Client,
url: &str,
token: Option<&str>,
) -> reqwest::blocking::RequestBuilder {
let mut req = client.get(url);
if let Some(token) = token {
req = req.bearer_auth(token);
}
req
}
pub fn update(project_root: &Path) -> Result<()> {
println!("Fetching contributors from GitHub...");
let token = get_github_token();
if token.is_some() {
println!(" using authenticated requests");
} else {
println!(" no token found, using unauthenticated requests (may hit rate limits)");
println!(" tip: install `gh` CLI and run `gh auth login` for higher limits");
}
let client = reqwest::blocking::Client::builder()
.user_agent("spacedrive-xtask")
.timeout(std::time::Duration::from_secs(20))
.build()
.context("Failed to build HTTP client")?;
// Paginate through all contributors
let mut all_contributors = Vec::new();
let mut page = 1u32;
loop {
let url = format!(
"https://api.github.com/repos/{}/contributors?per_page=100&page={}",
REPO, page
);
let resp: Vec<GitHubContributor> = github_get(&client, &url, token.as_deref())
.send()
.context("Failed to fetch contributors")?
.json()
.context("Failed to parse contributors response")?;
if resp.is_empty() {
break;
}
all_contributors.extend(resp);
page += 1;
}
// Filter out bots and excluded accounts
let humans: Vec<_> = all_contributors
.iter()
.filter(|c| c.account_type == "User" && !EXCLUDED_LOGINS.contains(&c.login.as_str()))
.collect();
println!("Found {} contributors, resolving names...", humans.len());
let mut contributors = Vec::new();
for (i, contributor) in humans.iter().enumerate() {
let name = match resolve_name(&client, &contributor.login, token.as_deref()) {
Ok(Some(n)) => n,
_ => contributor.login.clone(),
};
contributors.push(Contributor {
name,
github: contributor.login.clone(),
});
// Progress indicator every 25 users
if (i + 1) % 25 == 0 {
println!(" resolved {}/{}", i + 1, humans.len());
}
}
println!(" resolved {}/{}", contributors.len(), humans.len());
let output_path = project_root.join(OUTPUT_PATH);
let json = serde_json::to_string_pretty(&contributors)
.context("Failed to serialize contributors")?;
std::fs::write(&output_path, format!("{}\n", json))
.context("Failed to write contributors.json")?;
println!(
"Wrote {} contributors to {}",
contributors.len(),
OUTPUT_PATH
);
Ok(())
}
fn resolve_name(
client: &reqwest::blocking::Client,
login: &str,
token: Option<&str>,
) -> Result<Option<String>> {
let url = format!("https://api.github.com/users/{}", login);
let user: GitHubUser = github_get(client, &url, token)
.send()
.context("Failed to fetch user")?
.error_for_status()
.context("GitHub API returned an error")?
.json()
.context("Failed to parse user response")?;
Ok(user.name.filter(|n: &String| !n.is_empty()))
}

View File

@@ -26,6 +26,7 @@
mod bump;
mod config;
mod contributors;
mod native_deps;
mod system;
mod test_core;
@@ -69,6 +70,9 @@ fn main() -> Result<()> {
eprintln!(" build-mobile Build sd-mobile-core for React Native iOS/Android");
eprintln!(" test-core Run all core integration tests with progress tracking");
eprintln!(" bump <ver> Bump version across all packages (e.g. bump 2.0.0-alpha.2)");
eprintln!(
" update-contributors Fetch contributors from GitHub and update contributors.json"
);
eprintln!();
eprintln!("Examples:");
eprintln!(" cargo xtask setup # First time setup");
@@ -99,6 +103,10 @@ fn main() -> Result<()> {
let root = find_workspace_root()?;
bump::bump(&root, &version)?;
}
"update-contributors" => {
let project_root = find_workspace_root()?;
contributors::update(&project_root)?;
}
_ => {
eprintln!("Unknown command: {}", args[1]);
eprintln!("Run 'cargo xtask' for usage information.");
@@ -273,7 +281,10 @@ fn setup() -> Result<()> {
// Create target-suffixed daemon binary for Tauri bundler
// Tauri's externalBin appends the target triple to binary names
let exe_ext = if cfg!(windows) { ".exe" } else { "" };
let daemon_source = project_root.join(format!("target/{}/release/sd-daemon{}", target_triple, exe_ext));
let daemon_source = project_root.join(format!(
"target/{}/release/sd-daemon{}",
target_triple, exe_ext
));
let daemon_target = project_root.join(format!(
"target/release/sd-daemon-{}{}",
target_triple, exe_ext
@@ -534,7 +545,14 @@ fn build_mobile() -> Result<()> {
println!(" Building for iOS {} ({})...", name, target);
let status = Command::new("cargo")
.args(["build", "--release", "-p", "sd-mobile-core", "--target", target])
.args([
"build",
"--release",
"-p",
"sd-mobile-core",
"--target",
target,
])
.current_dir(&project_root)
.env("IPHONEOS_DEPLOYMENT_TARGET", "18.0")
.status()
@@ -571,7 +589,14 @@ fn build_mobile() -> Result<()> {
println!(" Building for Android {} ({})...", name, target);
let status = Command::new("cargo")
.args(["build", "--release", "-p", "sd-mobile-core", "--target", target])
.args([
"build",
"--release",
"-p",
"sd-mobile-core",
"--target",
target,
])
.current_dir(&project_root)
.status()
.context(format!("Failed to build for {}", target))?;