From ed02ba872c02eafed45f406c68d389d0a1112a4e Mon Sep 17 00:00:00 2001 From: cashmere Date: Thu, 30 Jul 2026 18:17:45 +0200 Subject: feat: Electron desktop app, native ffmpeg export, fully static web - apps/desktop: Electron shell serving the static web build over a privileged opencut:// protocol; exports stream frames over IPC to a bundled ffmpeg (libx264/libvpx-vp9, BT.709 tagged) with backpressure. electron-builder packaging: AppImage, deb, rpm. - apps/web: dead backend deleted (better-auth, drizzle/pg, upstash, botid, freesound proxy, blog, rss, env validation); next build emits a zero-env static export; /editor/[project_id] -> /editor?project=. - Native exporter mirrors SceneExporter; activates when the preload bridge is present; web keeps the mediabunny path. - Docker self-hosting now builds and serves the static export via nginx. - Release workflow: v* tags publish static web tarball + Linux packages. - Changelog 0.4.0; README/AGENTS/CONTRIBUTING/roadmap updated. - GPUI/Rust rewrite parked on branch rust-rewrite. --- .direnv/bin/nix-direnv-reload | 19 + .../bh1j42fx8nx7ga3939rjwfps5c0iz86f-source | 1 + .../llgwlxshmy0ifvxh7f8wq53vk5x7vd13-source | 1 + ...rofile-a5d5b61aa8a61b7d9d765e1daf971a9a578f1cfa | 1 + ...ile-a5d5b61aa8a61b7d9d765e1daf971a9a578f1cfa.rc | 2177 ++++++++++++++++++++ .dockerignore | 5 + .github/CONTRIBUTING.md | 144 +- .github/workflows/bun-ci.yml | 11 - .github/workflows/release.yml | 56 + AGENTS.md | 33 +- Cargo.toml | 1 - README.md | 164 +- apps/desktop/.gitignore | 4 + apps/desktop/Cargo.toml | 11 - apps/desktop/README.md | 47 - apps/desktop/build/icon.png | Bin 0 -> 741 bytes apps/desktop/electron-builder.yml | 28 + apps/desktop/package.json | 25 + apps/desktop/script/setup | 110 - apps/desktop/script/setup.ps1 | 27 - apps/desktop/scripts/copy-web.mjs | 18 + apps/desktop/src/export.ts | 293 +++ apps/desktop/src/main.rs | 41 - apps/desktop/src/main.ts | 131 ++ apps/desktop/src/preload.ts | 22 + apps/desktop/tsconfig.json | 17 + apps/web/.env.example | 21 - apps/web/.gitignore | 3 + apps/web/Dockerfile | 83 +- apps/web/content-collections.ts | 2 +- apps/web/drizzle.config.ts | 26 - apps/web/migrations/0000_brainy_saracen.sql | 62 - apps/web/migrations/meta/0000_snapshot.json | 344 ---- apps/web/migrations/meta/_journal.json | 13 - apps/web/next.config.ts | 45 +- apps/web/open-next.config.ts | 3 - apps/web/package.json | 28 +- apps/web/src/app/api/auth/[...all]/route.ts | 4 - apps/web/src/app/api/health/route.ts | 3 - apps/web/src/app/api/sounds/search/route.ts | 280 --- apps/web/src/app/blog/[slug]/page.tsx | 135 -- apps/web/src/app/blog/page.tsx | 52 - apps/web/src/app/changelog/[version]/page.tsx | 2 + apps/web/src/app/editor/[project_id]/page.tsx | 209 -- apps/web/src/app/editor/page.tsx | 217 ++ apps/web/src/app/layout.tsx | 12 +- apps/web/src/app/projects/page.tsx | 10 +- apps/web/src/app/robots.ts | 2 + apps/web/src/app/rss.xml/route.ts | 46 - apps/web/src/app/sitemap.ts | 100 +- apps/web/src/auth/client.ts | 6 - apps/web/src/auth/rate-limit.ts | 21 - apps/web/src/auth/server.ts | 43 - apps/web/src/blog/query.ts | 133 -- apps/web/src/blog/types.ts | 92 - apps/web/src/changelog/entries/0.4.0.md | 50 +- apps/web/src/components/editor/export-button.tsx | 18 + .../src/components/providers/editor-provider.tsx | 2 +- apps/web/src/core/managers/renderer-manager.ts | 42 +- apps/web/src/db/index.ts | 19 - apps/web/src/db/schema.ts | 70 - apps/web/src/desktop/bridge.ts | 35 + apps/web/src/env/web.ts | 30 - apps/web/src/export/index.ts | 1 + apps/web/src/services/renderer/native-exporter.ts | 159 ++ apps/web/wrangler.jsonc | 17 - bun.lock | 1182 ++++------- docker-compose.yml | 80 +- justfile | 10 +- notes/roadmap.md | 84 +- package.json | 7 +- script/setup-rust | 21 - script/setup-rust.ps1 | 23 - wrangler.jsonc | 16 - 74 files changed, 3896 insertions(+), 3354 deletions(-) create mode 100755 .direnv/bin/nix-direnv-reload create mode 120000 .direnv/flake-inputs/bh1j42fx8nx7ga3939rjwfps5c0iz86f-source create mode 120000 .direnv/flake-inputs/llgwlxshmy0ifvxh7f8wq53vk5x7vd13-source create mode 120000 .direnv/flake-profile-a5d5b61aa8a61b7d9d765e1daf971a9a578f1cfa create mode 100644 .direnv/flake-profile-a5d5b61aa8a61b7d9d765e1daf971a9a578f1cfa.rc create mode 100644 .github/workflows/release.yml create mode 100644 apps/desktop/.gitignore delete mode 100644 apps/desktop/Cargo.toml delete mode 100644 apps/desktop/README.md create mode 100644 apps/desktop/build/icon.png create mode 100644 apps/desktop/electron-builder.yml create mode 100644 apps/desktop/package.json delete mode 100644 apps/desktop/script/setup delete mode 100644 apps/desktop/script/setup.ps1 create mode 100644 apps/desktop/scripts/copy-web.mjs create mode 100644 apps/desktop/src/export.ts delete mode 100644 apps/desktop/src/main.rs create mode 100644 apps/desktop/src/main.ts create mode 100644 apps/desktop/src/preload.ts create mode 100644 apps/desktop/tsconfig.json delete mode 100644 apps/web/.env.example delete mode 100644 apps/web/drizzle.config.ts delete mode 100644 apps/web/migrations/0000_brainy_saracen.sql delete mode 100644 apps/web/migrations/meta/0000_snapshot.json delete mode 100644 apps/web/migrations/meta/_journal.json delete mode 100644 apps/web/open-next.config.ts delete mode 100644 apps/web/src/app/api/auth/[...all]/route.ts delete mode 100644 apps/web/src/app/api/health/route.ts delete mode 100644 apps/web/src/app/api/sounds/search/route.ts delete mode 100644 apps/web/src/app/blog/[slug]/page.tsx delete mode 100644 apps/web/src/app/blog/page.tsx delete mode 100644 apps/web/src/app/editor/[project_id]/page.tsx create mode 100644 apps/web/src/app/editor/page.tsx delete mode 100644 apps/web/src/app/rss.xml/route.ts delete mode 100644 apps/web/src/auth/client.ts delete mode 100644 apps/web/src/auth/rate-limit.ts delete mode 100644 apps/web/src/auth/server.ts delete mode 100644 apps/web/src/blog/query.ts delete mode 100644 apps/web/src/blog/types.ts delete mode 100644 apps/web/src/db/index.ts delete mode 100644 apps/web/src/db/schema.ts create mode 100644 apps/web/src/desktop/bridge.ts delete mode 100644 apps/web/src/env/web.ts create mode 100644 apps/web/src/services/renderer/native-exporter.ts delete mode 100644 apps/web/wrangler.jsonc delete mode 100644 script/setup-rust delete mode 100644 script/setup-rust.ps1 delete mode 100644 wrangler.jsonc diff --git a/.direnv/bin/nix-direnv-reload b/.direnv/bin/nix-direnv-reload new file mode 100755 index 00000000..22154755 --- /dev/null +++ b/.direnv/bin/nix-direnv-reload @@ -0,0 +1,19 @@ +#!/usr/bin/env bash +set -e +if [[ ! -d "/home/cashmere/opencut-classic" ]]; then + echo "Cannot find source directory; Did you move it?" + echo "(Looking for "/home/cashmere/opencut-classic")" + echo 'Cannot force reload with this script - use "direnv reload" manually and then try again' + exit 1 +fi + +# rebuild the cache forcefully +_nix_direnv_force_reload=1 direnv exec "/home/cashmere/opencut-classic" true + +# Update the mtime for .envrc. +# This will cause direnv to reload again - but without re-building. +touch "/home/cashmere/opencut-classic/.envrc" + +# Also update the timestamp of whatever profile_rc we have. +# This makes sure that we know we are up to date. +touch -r "/home/cashmere/opencut-classic/.envrc" "/home/cashmere/opencut-classic/.direnv"/*.rc diff --git a/.direnv/flake-inputs/bh1j42fx8nx7ga3939rjwfps5c0iz86f-source b/.direnv/flake-inputs/bh1j42fx8nx7ga3939rjwfps5c0iz86f-source new file mode 120000 index 00000000..26ef1015 --- /dev/null +++ b/.direnv/flake-inputs/bh1j42fx8nx7ga3939rjwfps5c0iz86f-source @@ -0,0 +1 @@ +/nix/store/bh1j42fx8nx7ga3939rjwfps5c0iz86f-source \ No newline at end of file diff --git a/.direnv/flake-inputs/llgwlxshmy0ifvxh7f8wq53vk5x7vd13-source b/.direnv/flake-inputs/llgwlxshmy0ifvxh7f8wq53vk5x7vd13-source new file mode 120000 index 00000000..d5db22c1 --- /dev/null +++ b/.direnv/flake-inputs/llgwlxshmy0ifvxh7f8wq53vk5x7vd13-source @@ -0,0 +1 @@ +/nix/store/llgwlxshmy0ifvxh7f8wq53vk5x7vd13-source \ No newline at end of file diff --git a/.direnv/flake-profile-a5d5b61aa8a61b7d9d765e1daf971a9a578f1cfa b/.direnv/flake-profile-a5d5b61aa8a61b7d9d765e1daf971a9a578f1cfa new file mode 120000 index 00000000..5191efdf --- /dev/null +++ b/.direnv/flake-profile-a5d5b61aa8a61b7d9d765e1daf971a9a578f1cfa @@ -0,0 +1 @@ +/nix/store/9pb6qva1c94cv5m1c0qkzfikf1kjdfzc-nix-shell-env \ No newline at end of file diff --git a/.direnv/flake-profile-a5d5b61aa8a61b7d9d765e1daf971a9a578f1cfa.rc b/.direnv/flake-profile-a5d5b61aa8a61b7d9d765e1daf971a9a578f1cfa.rc new file mode 100644 index 00000000..781b439b --- /dev/null +++ b/.direnv/flake-profile-a5d5b61aa8a61b7d9d765e1daf971a9a578f1cfa.rc @@ -0,0 +1,2177 @@ +unset shellHook +PATH=${PATH:-} +nix_saved_PATH="$PATH" +XDG_DATA_DIRS=${XDG_DATA_DIRS:-} +nix_saved_XDG_DATA_DIRS="$XDG_DATA_DIRS" +AR='ar' +export AR +AS='as' +export AS +BASH='/nix/store/c9mv5v53f7vy3wd00ah0f77k509mzmn7-bash-5.3p15/bin/bash' +CC='gcc' +export CC +CONFIG_SHELL='/nix/store/c9mv5v53f7vy3wd00ah0f77k509mzmn7-bash-5.3p15/bin/bash' +export CONFIG_SHELL +CXX='g++' +export CXX +HOSTTYPE='x86_64' +HOST_PATH='/nix/store/mp8s10fwm685azvvv1qq7zyf7iajjlj8-coreutils-9.11/bin:/nix/store/m1p6gxgxis75rn0d549ny2y8gpxjv9pd-findutils-4.10.0/bin:/nix/store/bx0a1ijcg9ss307lrizi1cavyfs4s2g5-diffutils-3.12/bin:/nix/store/x2kfd3hnhqk8d6kxkyffpk7lyaz8iy76-gnused-4.10/bin:/nix/store/xj9dgyqrcq8hrf4mrkvbcp4pa3hgrbhy-gnugrep-3.12/bin:/nix/store/0fpnmyzv3jv5a3q3p3f08hfmy2qrhmnj-gawk-5.4.1/bin:/nix/store/j5r2kzhzb2x8avna8rb21xq7dxrrr6wv-gnutar-1.35/bin:/nix/store/5chmw9ib7xz2f4rw6bgdwxx0vb9bqqpa-gzip-1.14/bin:/nix/store/mbakq7rmq5sf04xljvvaj19d5xgzr1zr-bzip2-1.0.8-bin/bin:/nix/store/xrj3v3k9v5hvx3vl0jgfp5if3syqgh8g-gnumake-4.4.1/bin:/nix/store/c9mv5v53f7vy3wd00ah0f77k509mzmn7-bash-5.3p15/bin:/nix/store/xbdvi1a0mqzml869i0123pylhgjdyqi5-patch-2.8/bin:/nix/store/6gkcds8g0vi54xrmxpyv58r8x2fvqw79-xz-5.8.3-bin/bin:/nix/store/w5rsmvy2fcp7lrnn6y73zgf1rsjk6wn0-file-5.48/bin' +export HOST_PATH +IFS=' '$'\x09'''$'\x0a''' +IN_NIX_SHELL='impure' +export IN_NIX_SHELL +LD='ld' +export LD +LINENO='101' +MACHTYPE='x86_64-pc-linux-gnu' +NIX_BINTOOLS='/nix/store/9q284bcg9jr5hd5537i7v9n2ngqm1dq8-binutils-wrapper-2.46' +export NIX_BINTOOLS +NIX_BINTOOLS_WRAPPER_TARGET_HOST_x86_64_unknown_linux_gnu='1' +export NIX_BINTOOLS_WRAPPER_TARGET_HOST_x86_64_unknown_linux_gnu +NIX_BUILD_CORES='28' +export NIX_BUILD_CORES +NIX_CC='/nix/store/adcz0m6qq2flmshdf0zz2xwjr5zbq1gr-gcc-wrapper-15.3.0' +export NIX_CC +NIX_CC_WRAPPER_TARGET_HOST_x86_64_unknown_linux_gnu='1' +export NIX_CC_WRAPPER_TARGET_HOST_x86_64_unknown_linux_gnu +NIX_CFLAGS_COMPILE=' -frandom-seed=9pb6qva1c9 -isystem /nix/store/x98gls54ki3fmm2pv2cmi6z8mcda6glk-nodejs-24.18.0/include -isystem /nix/store/x98gls54ki3fmm2pv2cmi6z8mcda6glk-nodejs-24.18.0/include' +export NIX_CFLAGS_COMPILE +NIX_ENFORCE_NO_NATIVE='1' +export NIX_ENFORCE_NO_NATIVE +NIX_HARDENING_ENABLE='bindnow format fortify fortify3 libcxxhardeningfast pic relro stackclashprotection stackprotector strictflexarrays1 strictoverflow zerocallusedregs' +export NIX_HARDENING_ENABLE +NIX_LDFLAGS='-rpath /home/cashmere/opencut-classic/outputs/out/lib ' +export NIX_LDFLAGS +NIX_NO_SELF_RPATH='1' +NIX_STORE='/nix/store' +export NIX_STORE +NM='nm' +export NM +OBJCOPY='objcopy' +export OBJCOPY +OBJDUMP='objdump' +export OBJDUMP +OLDPWD='' +export OLDPWD +OPTERR='1' +OSTYPE='linux-gnu' +PATH='/nix/store/fs2axbvhmim3y1dwzpf5mh56569h15df-bun-1.3.13/bin:/nix/store/70h070nb8c1l202a4gn58n6mxapbjhl9-just-1.57.0/bin:/nix/store/x98gls54ki3fmm2pv2cmi6z8mcda6glk-nodejs-24.18.0/bin:/nix/store/f13cgj4sypbjrwwpshfc2x4fd11is8dp-patchelf-0.15.2/bin:/nix/store/adcz0m6qq2flmshdf0zz2xwjr5zbq1gr-gcc-wrapper-15.3.0/bin:/nix/store/cmjirs6g57ddbyjglwic2g2cjrwy3ppr-gcc-15.3.0/bin:/nix/store/wnkmdd3nm0af5lxgwq1h4v52y6mdl4vj-glibc-2.42-67-bin/bin:/nix/store/mp8s10fwm685azvvv1qq7zyf7iajjlj8-coreutils-9.11/bin:/nix/store/9q284bcg9jr5hd5537i7v9n2ngqm1dq8-binutils-wrapper-2.46/bin:/nix/store/b01rwyg0ii5v8r50yrikcs5af64x0v0q-binutils-2.46/bin:/nix/store/mp8s10fwm685azvvv1qq7zyf7iajjlj8-coreutils-9.11/bin:/nix/store/m1p6gxgxis75rn0d549ny2y8gpxjv9pd-findutils-4.10.0/bin:/nix/store/bx0a1ijcg9ss307lrizi1cavyfs4s2g5-diffutils-3.12/bin:/nix/store/x2kfd3hnhqk8d6kxkyffpk7lyaz8iy76-gnused-4.10/bin:/nix/store/xj9dgyqrcq8hrf4mrkvbcp4pa3hgrbhy-gnugrep-3.12/bin:/nix/store/0fpnmyzv3jv5a3q3p3f08hfmy2qrhmnj-gawk-5.4.1/bin:/nix/store/j5r2kzhzb2x8avna8rb21xq7dxrrr6wv-gnutar-1.35/bin:/nix/store/5chmw9ib7xz2f4rw6bgdwxx0vb9bqqpa-gzip-1.14/bin:/nix/store/mbakq7rmq5sf04xljvvaj19d5xgzr1zr-bzip2-1.0.8-bin/bin:/nix/store/xrj3v3k9v5hvx3vl0jgfp5if3syqgh8g-gnumake-4.4.1/bin:/nix/store/c9mv5v53f7vy3wd00ah0f77k509mzmn7-bash-5.3p15/bin:/nix/store/xbdvi1a0mqzml869i0123pylhgjdyqi5-patch-2.8/bin:/nix/store/6gkcds8g0vi54xrmxpyv58r8x2fvqw79-xz-5.8.3-bin/bin:/nix/store/w5rsmvy2fcp7lrnn6y73zgf1rsjk6wn0-file-5.48/bin' +export PATH +PS4='+ ' +RANLIB='ranlib' +export RANLIB +READELF='readelf' +export READELF +SHELL='/nix/store/c9mv5v53f7vy3wd00ah0f77k509mzmn7-bash-5.3p15/bin/bash' +export SHELL +SIZE='size' +export SIZE +SOURCE_DATE_EPOCH='315532800' +export SOURCE_DATE_EPOCH +STRINGS='strings' +export STRINGS +STRIP='strip' +export STRIP +XDG_DATA_DIRS='/nix/store/fs2axbvhmim3y1dwzpf5mh56569h15df-bun-1.3.13/share:/nix/store/70h070nb8c1l202a4gn58n6mxapbjhl9-just-1.57.0/share:/nix/store/x98gls54ki3fmm2pv2cmi6z8mcda6glk-nodejs-24.18.0/share:/nix/store/f13cgj4sypbjrwwpshfc2x4fd11is8dp-patchelf-0.15.2/share' +export XDG_DATA_DIRS +__structuredAttrs='' +export __structuredAttrs +_substituteStream_has_warned_replace_deprecation='false' +buildInputs='' +export buildInputs +buildPhase='{ echo "------------------------------------------------------------";'$'\x0a'' echo " WARNING: the existence of this path is not guaranteed.";'$'\x0a'' echo " It is an internal implementation detail for pkgs.mkShell.";'$'\x0a'' echo "------------------------------------------------------------";'$'\x0a'' echo;'$'\x0a'' # Record all build inputs as runtime dependencies'$'\x0a'' export;'$'\x0a''} >> "$out"'$'\x0a''' +export buildPhase +builder='/nix/store/c9mv5v53f7vy3wd00ah0f77k509mzmn7-bash-5.3p15/bin/bash' +export builder +checkPhase='justCheckPhase' +cmakeFlags='' +export cmakeFlags +configureFlags='' +export configureFlags +defaultBuildInputs='' +defaultNativeBuildInputs='/nix/store/f13cgj4sypbjrwwpshfc2x4fd11is8dp-patchelf-0.15.2 /nix/store/wkyl6bvcp2dcw1in5qhqrq1dbvrwi193-update-autotools-gnu-config-scripts-hook /nix/store/0y5xmdb7qfvimjwbq7ibg1xdgkgjwqng-no-broken-symlinks.sh /nix/store/cv1d7p48379km6a85h4zp6kr86brh32q-audit-tmpdir.sh /nix/store/85clx3b0xkdf58jn161iy80y5223ilbi-compress-man-pages.sh /nix/store/p3l1a5y7nllfyrjn2krlwgcc3z0cd3fq-make-symlinks-relative.sh /nix/store/5yzw0vhkyszf2d179m0qfkgxmp5wjjx4-move-docs.sh /nix/store/fyaryjvghbkpfnsyw97hb3lyb37s1pd6-move-lib64.sh /nix/store/kd4xwxjpjxi71jkm6ka0np72if9rm3y0-move-sbin.sh /nix/store/pag6l61paj1dc9sv15l7bm5c17xn5kyk-move-systemd-user-units.sh /nix/store/cmzya9irvxzlkh7lfy6i82gbp0saxqj3-multiple-outputs.sh /nix/store/x8c40nfigps493a07sdr2pm5s9j1cdc0-patch-shebangs.sh /nix/store/cickvswrvann041nqxb0rxilc46svw1n-prune-libtool-files.sh /nix/store/xyff06pkhki3qy1ls77w10s0v79c9il0-reproducible-builds.sh /nix/store/z7k98578dfzi6l3hsvbivzm7hfqlk0zc-set-source-date-epoch-to-latest.sh /nix/store/pilsssjjdxvdphlg2h19p0bfx5q0jzkn-strip.sh /nix/store/adcz0m6qq2flmshdf0zz2xwjr5zbq1gr-gcc-wrapper-15.3.0' +depsBuildBuild='' +export depsBuildBuild +depsBuildBuildPropagated='' +export depsBuildBuildPropagated +depsBuildTarget='' +export depsBuildTarget +depsBuildTargetPropagated='' +export depsBuildTargetPropagated +depsHostHost='' +export depsHostHost +depsHostHostPropagated='' +export depsHostHostPropagated +depsTargetTarget='' +export depsTargetTarget +depsTargetTargetPropagated='' +export depsTargetTargetPropagated +doCheck='' +export doCheck +doInstallCheck='' +export doInstallCheck +dontAddDisableDepTrack='1' +export dontAddDisableDepTrack +declare -a envBuildBuildHooks=() +declare -a envBuildHostHooks=() +declare -a envBuildTargetHooks=() +declare -a envHostHostHooks=('ccWrapper_addCVars' 'bintoolsWrapper_addLDVars' ) +declare -a envHostTargetHooks=('ccWrapper_addCVars' 'bintoolsWrapper_addLDVars' ) +declare -a envTargetTargetHooks=() +declare -a fixupOutputHooks=('if [ -z "${dontPatchELF-}" ]; then patchELF "$prefix"; fi' 'if [[ -z "${noAuditTmpdir-}" && -e "$prefix" ]]; then auditTmpdir "$prefix"; fi' 'if [ -z "${dontGzipMan-}" ]; then compressManPages "$prefix"; fi' '_moveLib64' '_moveSbin' '_moveSystemdUserUnits' 'patchShebangsAuto' '_pruneLibtoolFiles' '_doStrip' ) +guess='28' +initialPath='/nix/store/mp8s10fwm685azvvv1qq7zyf7iajjlj8-coreutils-9.11 /nix/store/m1p6gxgxis75rn0d549ny2y8gpxjv9pd-findutils-4.10.0 /nix/store/bx0a1ijcg9ss307lrizi1cavyfs4s2g5-diffutils-3.12 /nix/store/x2kfd3hnhqk8d6kxkyffpk7lyaz8iy76-gnused-4.10 /nix/store/xj9dgyqrcq8hrf4mrkvbcp4pa3hgrbhy-gnugrep-3.12 /nix/store/0fpnmyzv3jv5a3q3p3f08hfmy2qrhmnj-gawk-5.4.1 /nix/store/j5r2kzhzb2x8avna8rb21xq7dxrrr6wv-gnutar-1.35 /nix/store/5chmw9ib7xz2f4rw6bgdwxx0vb9bqqpa-gzip-1.14 /nix/store/mbakq7rmq5sf04xljvvaj19d5xgzr1zr-bzip2-1.0.8-bin /nix/store/xrj3v3k9v5hvx3vl0jgfp5if3syqgh8g-gnumake-4.4.1 /nix/store/c9mv5v53f7vy3wd00ah0f77k509mzmn7-bash-5.3p15 /nix/store/xbdvi1a0mqzml869i0123pylhgjdyqi5-patch-2.8 /nix/store/6gkcds8g0vi54xrmxpyv58r8x2fvqw79-xz-5.8.3-bin /nix/store/w5rsmvy2fcp7lrnn6y73zgf1rsjk6wn0-file-5.48' +installPhase='justInstallPhase' +mesonFlags='' +export mesonFlags +name='nix-shell-env' +export name +nativeBuildInputs='/nix/store/fs2axbvhmim3y1dwzpf5mh56569h15df-bun-1.3.13 /nix/store/70h070nb8c1l202a4gn58n6mxapbjhl9-just-1.57.0 /nix/store/x98gls54ki3fmm2pv2cmi6z8mcda6glk-nodejs-24.18.0' +export nativeBuildInputs +out='/home/cashmere/opencut-classic/outputs/out' +export out +outputBin='out' +outputDev='out' +outputDevdoc='REMOVE' +outputDevman='out' +outputDoc='out' +outputInclude='out' +outputInfo='out' +outputLib='out' +outputMan='out' +outputs='out' +export outputs +patches='' +export patches +phases='buildPhase' +export phases +pkg='/nix/store/adcz0m6qq2flmshdf0zz2xwjr5zbq1gr-gcc-wrapper-15.3.0' +declare -a pkgsBuildBuild=() +declare -a pkgsBuildHost=('/nix/store/fs2axbvhmim3y1dwzpf5mh56569h15df-bun-1.3.13' '/nix/store/70h070nb8c1l202a4gn58n6mxapbjhl9-just-1.57.0' '/nix/store/x98gls54ki3fmm2pv2cmi6z8mcda6glk-nodejs-24.18.0' '/nix/store/f13cgj4sypbjrwwpshfc2x4fd11is8dp-patchelf-0.15.2' '/nix/store/wkyl6bvcp2dcw1in5qhqrq1dbvrwi193-update-autotools-gnu-config-scripts-hook' '/nix/store/0y5xmdb7qfvimjwbq7ibg1xdgkgjwqng-no-broken-symlinks.sh' '/nix/store/cv1d7p48379km6a85h4zp6kr86brh32q-audit-tmpdir.sh' '/nix/store/85clx3b0xkdf58jn161iy80y5223ilbi-compress-man-pages.sh' '/nix/store/p3l1a5y7nllfyrjn2krlwgcc3z0cd3fq-make-symlinks-relative.sh' '/nix/store/5yzw0vhkyszf2d179m0qfkgxmp5wjjx4-move-docs.sh' '/nix/store/fyaryjvghbkpfnsyw97hb3lyb37s1pd6-move-lib64.sh' '/nix/store/kd4xwxjpjxi71jkm6ka0np72if9rm3y0-move-sbin.sh' '/nix/store/pag6l61paj1dc9sv15l7bm5c17xn5kyk-move-systemd-user-units.sh' '/nix/store/cmzya9irvxzlkh7lfy6i82gbp0saxqj3-multiple-outputs.sh' '/nix/store/x8c40nfigps493a07sdr2pm5s9j1cdc0-patch-shebangs.sh' '/nix/store/cickvswrvann041nqxb0rxilc46svw1n-prune-libtool-files.sh' '/nix/store/xyff06pkhki3qy1ls77w10s0v79c9il0-reproducible-builds.sh' '/nix/store/z7k98578dfzi6l3hsvbivzm7hfqlk0zc-set-source-date-epoch-to-latest.sh' '/nix/store/pilsssjjdxvdphlg2h19p0bfx5q0jzkn-strip.sh' '/nix/store/adcz0m6qq2flmshdf0zz2xwjr5zbq1gr-gcc-wrapper-15.3.0' '/nix/store/9q284bcg9jr5hd5537i7v9n2ngqm1dq8-binutils-wrapper-2.46' ) +declare -a pkgsBuildTarget=() +declare -a pkgsHostHost=() +declare -a pkgsHostTarget=() +declare -a pkgsTargetTarget=() +declare -a postFixupHooks=('noBrokenSymlinksInAllOutputs' '_makeSymlinksRelative' '_multioutPropagateDev' ) +declare -a postUnpackHooks=('_updateSourceDateEpochFromSourceRoot' ) +declare -a preConfigureHooks=('_multioutConfig' ) +preConfigurePhases=' updateAutotoolsGnuConfigScriptsPhase' +declare -a preFixupHooks=('_moveToShare' '_multioutDocs' '_multioutDevs' ) +preferLocalBuild='1' +export preferLocalBuild +prefix='/home/cashmere/opencut-classic/outputs/out' +declare -a propagatedBuildDepFiles=('propagated-build-build-deps' 'propagated-native-build-inputs' 'propagated-build-target-deps' ) +propagatedBuildInputs='' +export propagatedBuildInputs +declare -a propagatedHostDepFiles=('propagated-host-host-deps' 'propagated-build-inputs' ) +propagatedNativeBuildInputs='' +export propagatedNativeBuildInputs +declare -a propagatedTargetDepFiles=('propagated-target-target-deps' ) +shell='/nix/store/c9mv5v53f7vy3wd00ah0f77k509mzmn7-bash-5.3p15/bin/bash' +export shell +shellHook='' +export shellHook +stdenv='/nix/store/sxx9r2badzg82107kiwwm9jmzlf52mdb-stdenv-linux' +export stdenv +strictDeps='' +export strictDeps +system='x86_64-linux' +export system +declare -a unpackCmdHooks=('_defaultUnpack' ) +_activatePkgs () +{ + + local hostOffset targetOffset; + local pkg; + for hostOffset in "${allPlatOffsets[@]}"; + do + local pkgsVar="${pkgAccumVarVars[hostOffset + 1]}"; + for targetOffset in "${allPlatOffsets[@]}"; + do + (( hostOffset <= targetOffset )) || continue; + local pkgsRef="${pkgsVar}[$targetOffset - $hostOffset]"; + local pkgsSlice="${!pkgsRef}[@]"; + for pkg in ${!pkgsSlice+"${!pkgsSlice}"}; + do + activatePackage "$pkg" "$hostOffset" "$targetOffset"; + done; + done; + done +} +_addRpathPrefix () +{ + + if [ "${NIX_NO_SELF_RPATH:-0}" != 1 ]; then + export NIX_LDFLAGS="-rpath $1/lib ${NIX_LDFLAGS-}"; + fi +} +_addToEnv () +{ + + local depHostOffset depTargetOffset; + local pkg; + for depHostOffset in "${allPlatOffsets[@]}"; + do + local hookVar="${pkgHookVarVars[depHostOffset + 1]}"; + local pkgsVar="${pkgAccumVarVars[depHostOffset + 1]}"; + for depTargetOffset in "${allPlatOffsets[@]}"; + do + (( depHostOffset <= depTargetOffset )) || continue; + local hookRef="${hookVar}[$depTargetOffset - $depHostOffset]"; + if [[ -z "${strictDeps-}" ]]; then + local visitedPkgs=""; + for pkg in "${pkgsBuildBuild[@]}" "${pkgsBuildHost[@]}" "${pkgsBuildTarget[@]}" "${pkgsHostHost[@]}" "${pkgsHostTarget[@]}" "${pkgsTargetTarget[@]}"; + do + if [[ "$visitedPkgs" = *"$pkg"* ]]; then + continue; + fi; + runHook "${!hookRef}" "$pkg"; + visitedPkgs+=" $pkg"; + done; + else + local pkgsRef="${pkgsVar}[$depTargetOffset - $depHostOffset]"; + local pkgsSlice="${!pkgsRef}[@]"; + for pkg in ${!pkgsSlice+"${!pkgsSlice}"}; + do + runHook "${!hookRef}" "$pkg"; + done; + fi; + done; + done +} +_allFlags () +{ + + export system pname name version; + while IFS='' read -r varName; do + nixTalkativeLog "@${varName}@ -> ${!varName}"; + args+=("--subst-var" "$varName"); + done < <(awk 'BEGIN { for (v in ENVIRON) if (v ~ /^[a-z][a-zA-Z0-9_]*$/) print v }') +} +_assignFirst () +{ + + local varName="$1"; + local _var; + local REMOVE=REMOVE; + shift; + for _var in "$@"; + do + if [ -n "${!_var-}" ]; then + eval "${varName}"="${_var}"; + return; + fi; + done; + echo; + echo "error: _assignFirst: could not find a non-empty variable whose name to assign to ${varName}."; + echo " The following variables were all unset or empty:"; + echo " $*"; + if [ -z "${out:-}" ]; then + echo ' If you do not want an "out" output in your derivation, make sure to define'; + echo ' the other specific required outputs. This can be achieved by picking one'; + echo " of the above as an output."; + echo ' You do not have to remove "out" if you want to have a different default'; + echo ' output, because the first output is taken as a default.'; + echo; + fi; + return 1 +} +_callImplicitHook () +{ + + local def="$1"; + local hookName="$2"; + if declare -F "$hookName" > /dev/null; then + nixTalkativeLog "calling implicit '$hookName' function hook"; + "$hookName"; + else + if type -p "$hookName" > /dev/null; then + nixTalkativeLog "sourcing implicit '$hookName' script hook"; + source "$hookName"; + else + if [ -n "${!hookName:-}" ]; then + nixTalkativeLog "evaling implicit '$hookName' string hook"; + eval "${!hookName}"; + else + return "$def"; + fi; + fi; + fi +} +_defaultUnpack () +{ + + local fn="$1"; + local destination; + if [ -d "$fn" ]; then + destination="$(stripHash "$fn")"; + if [ -e "$destination" ]; then + echo "Cannot copy $fn to $destination: destination already exists!"; + echo "Did you specify two \"srcs\" with the same \"name\"?"; + return 1; + fi; + cp -r --preserve=timestamps --reflink=auto -- "$fn" "$destination"; + else + case "$fn" in + *.tar.xz | *.tar.lzma | *.txz) + ( XZ_OPT="--threads=$NIX_BUILD_CORES" xz -d < "$fn"; + true ) | tar xf - --mode=+w --warning=no-timestamp + ;; + *.tar | *.tar.* | *.tgz | *.tbz2 | *.tbz) + tar xf "$fn" --mode=+w --warning=no-timestamp + ;; + *) + return 1 + ;; + esac; + fi +} +_doStrip () +{ + + local -ra flags=(dontStripHost dontStripTarget); + local -ra debugDirs=(stripDebugList stripDebugListTarget); + local -ra allDirs=(stripAllList stripAllListTarget); + local -ra stripCmds=(STRIP STRIP_FOR_TARGET); + local -ra ranlibCmds=(RANLIB RANLIB_FOR_TARGET); + stripDebugList=${stripDebugList[*]:-lib lib32 lib64 libexec bin sbin Applications Library/Frameworks}; + stripDebugListTarget=${stripDebugListTarget[*]:-}; + stripAllList=${stripAllList[*]:-}; + stripAllListTarget=${stripAllListTarget[*]:-}; + local i; + for i in ${!stripCmds[@]}; + do + local -n flag="${flags[$i]}"; + local -n debugDirList="${debugDirs[$i]}"; + local -n allDirList="${allDirs[$i]}"; + local -n stripCmd="${stripCmds[$i]}"; + local -n ranlibCmd="${ranlibCmds[$i]}"; + if [[ -n "${dontStrip-}" || -n "${flag-}" ]] || ! type -f "${stripCmd-}" 2> /dev/null 1>&2; then + continue; + fi; + stripDirs "$stripCmd" "$ranlibCmd" "$debugDirList" "${stripDebugFlags[*]:--S -p}"; + stripDirs "$stripCmd" "$ranlibCmd" "$allDirList" "${stripAllFlags[*]:--s -p}"; + done +} +_eval () +{ + + if declare -F "$1" > /dev/null 2>&1; then + "$@"; + else + eval "$1"; + fi +} +_logHook () +{ + + if [[ -z ${NIX_LOG_FD-} ]]; then + return; + fi; + local hookKind="$1"; + local hookExpr="$2"; + shift 2; + if declare -F "$hookExpr" > /dev/null 2>&1; then + nixTalkativeLog "calling '$hookKind' function hook '$hookExpr'" "$@"; + else + if type -p "$hookExpr" > /dev/null; then + nixTalkativeLog "sourcing '$hookKind' script hook '$hookExpr'"; + else + if [[ "$hookExpr" != "_callImplicitHook"* ]]; then + local exprToOutput; + if [[ ${NIX_DEBUG:-0} -ge 5 ]]; then + exprToOutput="$hookExpr"; + else + local hookExprLine; + while IFS= read -r hookExprLine; do + hookExprLine="${hookExprLine#"${hookExprLine%%[![:space:]]*}"}"; + if [[ -n "$hookExprLine" ]]; then + exprToOutput+="$hookExprLine\\n "; + fi; + done <<< "$hookExpr"; + exprToOutput="${exprToOutput%%\\n }"; + fi; + nixTalkativeLog "evaling '$hookKind' string hook '$exprToOutput'"; + fi; + fi; + fi +} +_makeSymlinksRelative () +{ + + local prefixes; + prefixes=(); + for output in $(getAllOutputNames); + do + [ ! -e "${!output}" ] && continue; + prefixes+=("${!output}"); + done; + find "${prefixes[@]}" -type l -printf '%H\0%p\0' | xargs -0 -n2 -r -P "$NIX_BUILD_CORES" sh -c ' + output="$1" + link="$2" + + linkTarget=$(readlink "$link") + + # only touch links that point inside the same output tree + [[ $linkTarget == "$output"/* ]] || exit 0 + + if [ ! -e "$linkTarget" ]; then + echo "the symlink $link is broken, it points to $linkTarget (which is missing)" + fi + + echo "making symlink relative: $link" + ln -snrf "$linkTarget" "$link" + ' _ +} +_moveLib64 () +{ + + if [ "${dontMoveLib64-}" = 1 ]; then + return; + fi; + if [ ! -e "$prefix/lib64" -o -L "$prefix/lib64" ]; then + return; + fi; + echo "moving $prefix/lib64/* to $prefix/lib"; + mkdir -p $prefix/lib; + shopt -s dotglob; + for i in $prefix/lib64/*; + do + mv --no-clobber "$i" $prefix/lib; + done; + shopt -u dotglob; + rmdir $prefix/lib64; + ln -s lib $prefix/lib64 +} +_moveSbin () +{ + + if [ "${dontMoveSbin-}" = 1 ]; then + return; + fi; + if [ ! -e "$prefix/sbin" -o -L "$prefix/sbin" ]; then + return; + fi; + echo "moving $prefix/sbin/* to $prefix/bin"; + mkdir -p $prefix/bin; + shopt -s dotglob; + for i in $prefix/sbin/*; + do + mv "$i" $prefix/bin; + done; + shopt -u dotglob; + rmdir $prefix/sbin; + ln -s bin $prefix/sbin +} +_moveSystemdUserUnits () +{ + + if [ "${dontMoveSystemdUserUnits:-0}" = 1 ]; then + return; + fi; + if [ ! -e "${prefix:?}/lib/systemd/user" ]; then + return; + fi; + local source="$prefix/lib/systemd/user"; + local target="$prefix/share/systemd/user"; + echo "moving $source/* to $target"; + mkdir -p "$target"; + ( shopt -s dotglob; + for i in "$source"/*; + do + mv "$i" "$target"; + done ); + rmdir "$source"; + ln -s "$target" "$source" +} +_moveToShare () +{ + + if [ -n "$__structuredAttrs" ]; then + if [ -z "${forceShare-}" ]; then + forceShare=(man doc info); + fi; + else + forceShare=(${forceShare:-man doc info}); + fi; + if [[ -z "$out" ]]; then + return; + fi; + for d in "${forceShare[@]}"; + do + if [ -d "$out/$d" ]; then + if [ -d "$out/share/$d" ]; then + echo "both $d/ and share/$d/ exist!"; + else + echo "moving $out/$d to $out/share/$d"; + mkdir -p $out/share; + mv $out/$d $out/share/; + fi; + fi; + done +} +_multioutConfig () +{ + + if [ "$(getAllOutputNames)" = "out" ] || [ -z "${setOutputFlags-1}" ]; then + return; + fi; + if [ -z "${shareDocName:-}" ]; then + local confScript="${configureScript:-}"; + if [ -z "$confScript" ] && [ -x ./configure ]; then + confScript=./configure; + fi; + if [ -f "$confScript" ]; then + local shareDocName="$(sed -n "s/^PACKAGE_TARNAME='\(.*\)'$/\1/p" < "$confScript")"; + fi; + if [ -z "$shareDocName" ] || echo "$shareDocName" | grep -q '[^a-zA-Z0-9_-]'; then + shareDocName="$(echo "$name" | sed 's/-[^a-zA-Z].*//')"; + fi; + fi; + prependToVar configureFlags --bindir="${!outputBin}"/bin --sbindir="${!outputBin}"/sbin --includedir="${!outputInclude}"/include --mandir="${!outputMan}"/share/man --infodir="${!outputInfo}"/share/info --docdir="${!outputDoc}"/share/doc/"${shareDocName}" --libdir="${!outputLib}"/lib --libexecdir="${!outputLib}"/libexec --localedir="${!outputLib}"/share/locale; + prependToVar installFlags pkgconfigdir="${!outputDev}"/lib/pkgconfig m4datadir="${!outputDev}"/share/aclocal aclocaldir="${!outputDev}"/share/aclocal +} +_multioutDevs () +{ + + if [ "$(getAllOutputNames)" = "out" ] || [ -z "${moveToDev-1}" ]; then + return; + fi; + moveToOutput include "${!outputInclude}"; + moveToOutput lib/pkgconfig "${!outputDev}"; + moveToOutput share/pkgconfig "${!outputDev}"; + moveToOutput lib/cmake "${!outputDev}"; + moveToOutput share/aclocal "${!outputDev}"; + for f in "${!outputDev}"/{lib,share}/pkgconfig/*.pc; + do + echo "Patching '$f' includedir to output ${!outputInclude}"; + sed -i "/^includedir=/s,=\${prefix},=${!outputInclude}," "$f"; + done +} +_multioutDocs () +{ + + local REMOVE=REMOVE; + moveToOutput share/info "${!outputInfo}"; + moveToOutput share/doc "${!outputDoc}"; + moveToOutput share/gtk-doc "${!outputDevdoc}"; + moveToOutput share/devhelp/books "${!outputDevdoc}"; + moveToOutput share/man "${!outputMan}"; + moveToOutput share/man/man3 "${!outputDevman}" +} +_multioutPropagateDev () +{ + + if [ "$(getAllOutputNames)" = "out" ]; then + return; + fi; + local outputFirst; + for outputFirst in $(getAllOutputNames); + do + break; + done; + local propagaterOutput="$outputDev"; + if [ -z "$propagaterOutput" ]; then + propagaterOutput="$outputFirst"; + fi; + if [ -z "${propagatedBuildOutputs+1}" ]; then + local po_dirty="$outputBin $outputInclude $outputLib"; + set +o pipefail; + propagatedBuildOutputs=`echo "$po_dirty" | tr -s ' ' '\n' | grep -v -F "$propagaterOutput" | sort -u | tr '\n' ' ' `; + set -o pipefail; + fi; + if [ -z "$propagatedBuildOutputs" ]; then + return; + fi; + mkdir -p "${!propagaterOutput}"/nix-support; + for output in $propagatedBuildOutputs; + do + echo -n " ${!output}" >> "${!propagaterOutput}"/nix-support/propagated-build-inputs; + done +} +_nixLogWithLevel () +{ + + [[ -z ${NIX_LOG_FD-} || ${NIX_DEBUG:-0} -lt ${1:?} ]] && return 0; + local logLevel; + case "${1:?}" in + 0) + logLevel=ERROR + ;; + 1) + logLevel=WARN + ;; + 2) + logLevel=NOTICE + ;; + 3) + logLevel=INFO + ;; + 4) + logLevel=TALKATIVE + ;; + 5) + logLevel=CHATTY + ;; + 6) + logLevel=DEBUG + ;; + 7) + logLevel=VOMIT + ;; + *) + echo "_nixLogWithLevel: called with invalid log level: ${1:?}" >&"$NIX_LOG_FD"; + return 1 + ;; + esac; + local callerName="${FUNCNAME[2]}"; + if [[ $callerName == "_callImplicitHook" ]]; then + callerName="${hookName:?}"; + fi; + printf "%s: %s: %s\n" "$logLevel" "$callerName" "${2:?}" >&"$NIX_LOG_FD" +} +_overrideFirst () +{ + + if [ -z "${!1-}" ]; then + _assignFirst "$@"; + fi +} +_pruneLibtoolFiles () +{ + + if [ "${dontPruneLibtoolFiles-}" ] || [ ! -e "$prefix" ]; then + return; + fi; + find "$prefix" -type f -name '*.la' -exec grep -q '^# Generated by .*libtool' {} \; -exec grep -q "^old_library=''" {} \; -exec sed -i {} -e "/^dependency_libs='[^']/ c dependency_libs='' #pruned" \; +} +_updateSourceDateEpochFromSourceRoot () +{ + + if [ -n "$sourceRoot" ]; then + updateSourceDateEpoch "$sourceRoot"; + fi +} +activatePackage () +{ + + local pkg="$1"; + local -r hostOffset="$2"; + local -r targetOffset="$3"; + (( hostOffset <= targetOffset )) || exit 1; + if [ -f "$pkg" ]; then + nixTalkativeLog "sourcing setup hook '$pkg'"; + source "$pkg"; + fi; + if [[ -z "${strictDeps-}" || "$hostOffset" -le -1 ]]; then + addToSearchPath _PATH "$pkg/bin"; + fi; + if (( hostOffset <= -1 )); then + addToSearchPath _XDG_DATA_DIRS "$pkg/share"; + fi; + if [[ "$hostOffset" -eq 0 && -d "$pkg/bin" ]]; then + addToSearchPath _HOST_PATH "$pkg/bin"; + fi; + if [[ -f "$pkg/nix-support/setup-hook" ]]; then + nixTalkativeLog "sourcing setup hook '$pkg/nix-support/setup-hook'"; + source "$pkg/nix-support/setup-hook"; + fi +} +addEnvHooks () +{ + + local depHostOffset="$1"; + shift; + local pkgHookVarsSlice="${pkgHookVarVars[$depHostOffset + 1]}[@]"; + local pkgHookVar; + for pkgHookVar in "${!pkgHookVarsSlice}"; + do + eval "${pkgHookVar}s"'+=("$@")'; + done +} +addToSearchPath () +{ + + addToSearchPathWithCustomDelimiter ":" "$@" +} +addToSearchPathWithCustomDelimiter () +{ + + local delimiter="$1"; + local varName="$2"; + local dir="$3"; + if [[ -d "$dir" && "${!varName:+${delimiter}${!varName}${delimiter}}" != *"${delimiter}${dir}${delimiter}"* ]]; then + export "${varName}=${!varName:+${!varName}${delimiter}}${dir}"; + fi +} +appendToVar () +{ + + local -n nameref="$1"; + local useArray type; + if [ -n "$__structuredAttrs" ]; then + useArray=true; + else + useArray=false; + fi; + if type=$(declare -p "$1" 2> /dev/null); then + case "${type#* }" in + -A*) + echo "appendToVar(): ERROR: trying to use appendToVar on an associative array, use variable+=([\"X\"]=\"Y\") instead." 1>&2; + return 1 + ;; + -a*) + useArray=true + ;; + *) + useArray=false + ;; + esac; + fi; + shift; + if $useArray; then + nameref=(${nameref+"${nameref[@]}"} "$@"); + else + nameref="${nameref-} $*"; + fi +} +auditTmpdir () +{ + + local dir="$1"; + [ -e "$dir" ] || return 0; + echo "checking for references to $TMPDIR/ in $dir..."; + local tmpdir elf_fifo script_fifo; + tmpdir="$(mktemp -d)"; + elf_fifo="$tmpdir/elf"; + script_fifo="$tmpdir/script"; + mkfifo "$elf_fifo" "$script_fifo"; + ( find "$dir" -type f -not -path '*/.build-id/*' -print0 | while IFS= read -r -d '' file; do + if isELF "$file"; then + printf '%s\0' "$file" 1>&3; + else + if isScript "$file"; then + filename=${file##*/}; + dir=${file%/*}; + if [ -e "$dir/.$filename-wrapped" ]; then + printf '%s\0' "$file" 1>&4; + fi; + fi; + fi; + done; + exec 3>&- 4>&- ) 3> "$elf_fifo" 4> "$script_fifo" & ( xargs -0 -r -P "$NIX_BUILD_CORES" -n 1 sh -c ' + if { printf :; patchelf --print-rpath "$1"; } | grep -q -F ":$TMPDIR/"; then + echo "RPATH of binary $1 contains a forbidden reference to $TMPDIR/" + exit 1 + fi + ' _ < "$elf_fifo" ) & local pid_elf=$!; + local pid_script; + ( xargs -0 -r -P "$NIX_BUILD_CORES" -n 1 sh -c ' + if grep -q -F "$TMPDIR/" "$1"; then + echo "wrapper script $1 contains a forbidden reference to $TMPDIR/" + exit 1 + fi + ' _ < "$script_fifo" ) & local pid_script=$!; + wait "$pid_elf" || { + echo "Some binaries contain forbidden references to $TMPDIR/. Check the error above!"; + exit 1 + }; + wait "$pid_script" || { + echo "Some scripts contain forbidden references to $TMPDIR/. Check the error above!"; + exit 1 + }; + rm -r "$tmpdir" +} +bintoolsWrapper_addLDVars () +{ + + local role_post; + getHostRoleEnvHook; + if [[ -d "$1/lib64" && ! -L "$1/lib64" ]]; then + export NIX_LDFLAGS${role_post}+=" -L$1/lib64"; + fi; + if [[ -d "$1/lib" ]]; then + local -a glob=($1/lib/lib*); + if [ "${#glob[*]}" -gt 0 ]; then + export NIX_LDFLAGS${role_post}+=" -L$1/lib"; + fi; + fi +} +buildPhase () +{ + + runHook preBuild; + if [[ -z "${makeFlags-}" && -z "${makefile:-}" && ! ( -e Makefile || -e makefile || -e GNUmakefile ) ]]; then + echo "no Makefile or custom buildPhase, doing nothing"; + else + foundMakefile=1; + local flagsArray=(${enableParallelBuilding:+-j${NIX_BUILD_CORES}} SHELL="$SHELL"); + concatTo flagsArray makeFlags makeFlagsArray buildFlags buildFlagsArray; + echoCmd 'build flags' "${flagsArray[@]}"; + make ${makefile:+-f $makefile} "${flagsArray[@]}"; + unset flagsArray; + fi; + runHook postBuild +} +ccWrapper_addCVars () +{ + + local role_post; + getHostRoleEnvHook; + local found=; + if [ -d "$1/include" ]; then + export NIX_CFLAGS_COMPILE${role_post}+=" -isystem $1/include"; + found=1; + fi; + if [ -d "$1/Library/Frameworks" ]; then + export NIX_CFLAGS_COMPILE${role_post}+=" -iframework $1/Library/Frameworks"; + found=1; + fi; + if [[ -n "" && -n ${NIX_STORE:-} && -n $found ]]; then + local scrubbed="$NIX_STORE/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-${1#"$NIX_STORE"/*-}"; + export NIX_CFLAGS_COMPILE${role_post}+=" -fmacro-prefix-map=$1=$scrubbed"; + fi +} +checkPhase () +{ + + runHook preCheck; + if [[ -z "${foundMakefile:-}" ]]; then + echo "no Makefile or custom checkPhase, doing nothing"; + runHook postCheck; + return; + fi; + if [[ -z "${checkTarget:-}" ]]; then + if make -n ${makefile:+-f $makefile} check > /dev/null 2>&1; then + checkTarget="check"; + else + if make -n ${makefile:+-f $makefile} test > /dev/null 2>&1; then + checkTarget="test"; + fi; + fi; + fi; + if [[ -z "${checkTarget:-}" ]]; then + echo "no check/test target in ${makefile:-Makefile}, doing nothing"; + else + local flagsArray=(${enableParallelChecking:+-j${NIX_BUILD_CORES}} SHELL="$SHELL"); + concatTo flagsArray makeFlags makeFlagsArray checkFlags=VERBOSE=y checkFlagsArray checkTarget; + echoCmd 'check flags' "${flagsArray[@]}"; + make ${makefile:+-f $makefile} "${flagsArray[@]}"; + unset flagsArray; + fi; + runHook postCheck +} +compressManPages () +{ + + local dir="$1"; + if [ -L "$dir"/share ] || [ -L "$dir"/share/man ] || [ ! -d "$dir/share/man" ]; then + return; + fi; + echo "gzipping man pages under $dir/share/man/"; + find "$dir"/share/man/ -type f -a '!' -regex '.*\.\(bz2\|gz\|xz\)$' -print0 | xargs -0 -n1 -P "$NIX_BUILD_CORES" gzip -n -f; + find "$dir"/share/man/ -type l -a '!' -regex '.*\.\(bz2\|gz\|xz\)$' -print0 | sort -z | while IFS= read -r -d '' f; do + local target; + target="$(readlink -f "$f")"; + if [ -f "$target".gz ]; then + ln -sf "$target".gz "$f".gz && rm "$f"; + fi; + done +} +concatStringsSep () +{ + + local sep="$1"; + local name="$2"; + local type oldifs; + if type=$(declare -p "$name" 2> /dev/null); then + local -n nameref="$name"; + case "${type#* }" in + -A*) + echo "concatStringsSep(): ERROR: trying to use concatStringsSep on an associative array." 1>&2; + return 1 + ;; + -a*) + local IFS="$(printf '\036')" + ;; + *) + local IFS=" " + ;; + esac; + local ifs_separated="${nameref[*]}"; + echo -n "${ifs_separated//"$IFS"/"$sep"}"; + fi +} +concatTo () +{ + + local -; + set -o noglob; + local -n targetref="$1"; + shift; + local arg default name type; + for arg in "$@"; + do + IFS="=" read -r name default <<< "$arg"; + local -n nameref="$name"; + if [[ -z "${nameref[*]}" && -n "$default" ]]; then + targetref+=("$default"); + else + if type=$(declare -p "$name" 2> /dev/null); then + case "${type#* }" in + -A*) + echo "concatTo(): ERROR: trying to use concatTo on an associative array." 1>&2; + return 1 + ;; + -a*) + targetref+=("${nameref[@]}") + ;; + *) + if [[ "$name" = *"Array" ]]; then + nixErrorLog "concatTo(): $name is not declared as array, treating as a singleton. This will become an error in future"; + targetref+=(${nameref+"${nameref[@]}"}); + else + targetref+=(${nameref-}); + fi + ;; + esac; + fi; + fi; + done +} +configurePhase () +{ + + runHook preConfigure; + : "${configureScript=}"; + if [[ -z "$configureScript" && -x ./configure ]]; then + configureScript=./configure; + fi; + if [ -z "${dontFixLibtool:-}" ]; then + export lt_cv_deplibs_check_method="${lt_cv_deplibs_check_method-pass_all}"; + local i; + find . -iname "ltmain.sh" -print0 | while IFS='' read -r -d '' i; do + echo "fixing libtool script $i"; + fixLibtool "$i"; + done; + CONFIGURE_MTIME_REFERENCE=$(mktemp configure.mtime.reference.XXXXXX); + find . -executable -type f -name configure -exec grep -l 'GNU Libtool is free software; you can redistribute it and/or modify' {} \; -exec touch -r {} "$CONFIGURE_MTIME_REFERENCE" \; -exec sed -i s_/usr/bin/file_file_g {} \; -exec touch -r "$CONFIGURE_MTIME_REFERENCE" {} \;; + rm -f "$CONFIGURE_MTIME_REFERENCE"; + fi; + if [[ -z "${dontAddPrefix:-}" && -n "$prefix" ]]; then + local -r prefixKeyOrDefault="${prefixKey:---prefix=}"; + if [ "${prefixKeyOrDefault: -1}" = " " ]; then + prependToVar configureFlags "$prefix"; + prependToVar configureFlags "${prefixKeyOrDefault::-1}"; + else + prependToVar configureFlags "$prefixKeyOrDefault$prefix"; + fi; + fi; + if [[ -f "$configureScript" ]]; then + if [ -z "${dontAddDisableDepTrack:-}" ]; then + if grep -q dependency-tracking "$configureScript"; then + prependToVar configureFlags --disable-dependency-tracking; + fi; + fi; + if [ -z "${dontDisableStatic:-}" ]; then + if grep -q enable-static "$configureScript"; then + prependToVar configureFlags --disable-static; + fi; + fi; + if [ -z "${dontPatchShebangsInConfigure:-}" ]; then + patchShebangs --build "$configureScript"; + fi; + fi; + if [ -n "$configureScript" ]; then + local -a flagsArray; + concatTo flagsArray configureFlags configureFlagsArray; + echoCmd 'configure flags' "${flagsArray[@]}"; + $configureScript "${flagsArray[@]}"; + unset flagsArray; + else + echo "no configure script, doing nothing"; + fi; + runHook postConfigure +} +consumeEntire () +{ + + if IFS='' read -r -d '' "$1"; then + echo "consumeEntire(): ERROR: Input null bytes, won't process" 1>&2; + return 1; + fi +} +definePhases () +{ + + if [ -z "${phases[*]:-}" ]; then + phases="${prePhases[*]:-} unpackPhase patchPhase ${preConfigurePhases[*]:-} configurePhase ${preBuildPhases[*]:-} buildPhase checkPhase ${preInstallPhases[*]:-} installPhase ${preFixupPhases[*]:-} fixupPhase installCheckPhase ${preDistPhases[*]:-} distPhase ${postPhases[*]:-}"; + fi +} +distPhase () +{ + + runHook preDist; + local flagsArray=(); + concatTo flagsArray distFlags distFlagsArray distTarget=dist; + echo 'dist flags: %q' "${flagsArray[@]}"; + make ${makefile:+-f $makefile} "${flagsArray[@]}"; + if [ "${dontCopyDist:-0}" != 1 ]; then + mkdir -p "$out/tarballs"; + cp -pvd ${tarballs[*]:-*.tar.gz} "$out/tarballs"; + fi; + runHook postDist +} +dumpVars () +{ + + if [[ "${noDumpEnvVars:-0}" != 1 && -d "$NIX_BUILD_TOP" ]]; then + local old_umask; + old_umask=$(umask); + umask 0077; + export 2> /dev/null > "$NIX_BUILD_TOP/env-vars"; + umask "$old_umask"; + fi +} +echoCmd () +{ + + printf "%s:" "$1"; + shift; + printf ' %q' "$@"; + echo +} +exitHandler () +{ + + exitCode="$?"; + set +e; + if [ -n "${showBuildStats:-}" ]; then + read -r -d '' -a buildTimes < <(times); + echo "build times:"; + echo "user time for the shell ${buildTimes[0]}"; + echo "system time for the shell ${buildTimes[1]}"; + echo "user time for all child processes ${buildTimes[2]}"; + echo "system time for all child processes ${buildTimes[3]}"; + fi; + if (( "$exitCode" != 0 )); then + runHook failureHook; + if [ -n "${succeedOnFailure:-}" ]; then + echo "build failed with exit code $exitCode (ignored)"; + mkdir -p "$out/nix-support"; + printf "%s" "$exitCode" > "$out/nix-support/failed"; + exit 0; + fi; + else + runHook exitHook; + fi; + return "$exitCode" +} +findInputs () +{ + + local -r pkg="$1"; + local -r hostOffset="$2"; + local -r targetOffset="$3"; + (( hostOffset <= targetOffset )) || exit 1; + local varVar="${pkgAccumVarVars[hostOffset + 1]}"; + local varRef="$varVar[$((targetOffset - hostOffset))]"; + local var="${!varRef}"; + unset -v varVar varRef; + local varSlice="$var[*]"; + case " ${!varSlice-} " in + *" $pkg "*) + return 0 + ;; + esac; + unset -v varSlice; + eval "$var"'+=("$pkg")'; + if ! [ -e "$pkg" ]; then + echo "build input $pkg does not exist" 1>&2; + exit 1; + fi; + function mapOffset () + { + local -r inputOffset="$1"; + local -n outputOffset="$2"; + if (( inputOffset <= 0 )); then + outputOffset=$((inputOffset + hostOffset)); + else + outputOffset=$((inputOffset - 1 + targetOffset)); + fi + }; + local relHostOffset; + for relHostOffset in "${allPlatOffsets[@]}"; + do + local files="${propagatedDepFilesVars[relHostOffset + 1]}"; + local hostOffsetNext; + mapOffset "$relHostOffset" hostOffsetNext; + (( -1 <= hostOffsetNext && hostOffsetNext <= 1 )) || continue; + local relTargetOffset; + for relTargetOffset in "${allPlatOffsets[@]}"; + do + (( "$relHostOffset" <= "$relTargetOffset" )) || continue; + local fileRef="${files}[$relTargetOffset - $relHostOffset]"; + local file="${!fileRef}"; + unset -v fileRef; + local targetOffsetNext; + mapOffset "$relTargetOffset" targetOffsetNext; + (( -1 <= hostOffsetNext && hostOffsetNext <= 1 )) || continue; + [[ -f "$pkg/nix-support/$file" ]] || continue; + local pkgNext; + read -r -d '' pkgNext < "$pkg/nix-support/$file" || true; + for pkgNext in $pkgNext; + do + findInputs "$pkgNext" "$hostOffsetNext" "$targetOffsetNext"; + done; + done; + done +} +fixLibtool () +{ + + local search_path; + for flag in $NIX_LDFLAGS; + do + case $flag in + -L*) + search_path+=" ${flag#-L}" + ;; + esac; + done; + sed -i "$1" -e "s^eval \(sys_lib_search_path=\).*^\1'${search_path:-}'^" -e 's^eval sys_lib_.+search_path=.*^^' +} +fixupPhase () +{ + + local output; + for output in $(getAllOutputNames); + do + if [ -e "${!output}" ]; then + chmod -R u+w,u-s,g-s "${!output}"; + fi; + done; + runHook preFixup; + local output; + for output in $(getAllOutputNames); + do + prefix="${!output}" runHook fixupOutput; + done; + recordPropagatedDependencies; + if [ -n "${setupHook:-}" ]; then + mkdir -p "${!outputDev}/nix-support"; + substituteAll "$setupHook" "${!outputDev}/nix-support/setup-hook"; + fi; + if [ -n "${setupHooks:-}" ]; then + mkdir -p "${!outputDev}/nix-support"; + local hook; + for hook in ${setupHooks[@]}; + do + local content; + consumeEntire content < "$hook"; + substituteAllStream content "file '$hook'" >> "${!outputDev}/nix-support/setup-hook"; + unset -v content; + done; + unset -v hook; + fi; + if [ -n "${propagatedUserEnvPkgs[*]:-}" ]; then + mkdir -p "${!outputBin}/nix-support"; + printWords "${propagatedUserEnvPkgs[@]}" > "${!outputBin}/nix-support/propagated-user-env-packages"; + fi; + runHook postFixup +} +genericBuild () +{ + + export GZIP_NO_TIMESTAMPS=1; + if [ -f "${buildCommandPath:-}" ]; then + source "$buildCommandPath"; + return; + fi; + if [ -n "${buildCommand:-}" ]; then + eval "$buildCommand"; + return; + fi; + definePhases; + for curPhase in ${phases[*]}; + do + runPhase "$curPhase"; + done +} +getAllOutputNames () +{ + + if [ -n "$__structuredAttrs" ]; then + echo "${!outputs[*]}"; + else + echo "$outputs"; + fi +} +getHostRole () +{ + + getRole "$hostOffset" +} +getHostRoleEnvHook () +{ + + getRole "$depHostOffset" +} +getRole () +{ + + case $1 in + -1) + role_post='_FOR_BUILD' + ;; + 0) + role_post='' + ;; + 1) + role_post='_FOR_TARGET' + ;; + *) + echo "binutils-wrapper-2.46: used as improper sort of dependency" 1>&2; + return 1 + ;; + esac +} +getTargetRole () +{ + + getRole "$targetOffset" +} +getTargetRoleEnvHook () +{ + + getRole "$depTargetOffset" +} +getTargetRoleWrapper () +{ + + case $targetOffset in + -1) + export NIX_BINTOOLS_WRAPPER_TARGET_BUILD_x86_64_unknown_linux_gnu=1 + ;; + 0) + export NIX_BINTOOLS_WRAPPER_TARGET_HOST_x86_64_unknown_linux_gnu=1 + ;; + 1) + export NIX_BINTOOLS_WRAPPER_TARGET_TARGET_x86_64_unknown_linux_gnu=1 + ;; + *) + echo "binutils-wrapper-2.46: used as improper sort of dependency" 1>&2; + return 1 + ;; + esac +} +installCheckPhase () +{ + + runHook preInstallCheck; + if [[ -z "${foundMakefile:-}" ]]; then + echo "no Makefile or custom installCheckPhase, doing nothing"; + else + if [[ -z "${installCheckTarget:-}" ]] && ! make -n ${makefile:+-f $makefile} "${installCheckTarget:-installcheck}" > /dev/null 2>&1; then + echo "no installcheck target in ${makefile:-Makefile}, doing nothing"; + else + local flagsArray=(${enableParallelChecking:+-j${NIX_BUILD_CORES}} SHELL="$SHELL"); + concatTo flagsArray makeFlags makeFlagsArray installCheckFlags installCheckFlagsArray installCheckTarget=installcheck; + echoCmd 'installcheck flags' "${flagsArray[@]}"; + make ${makefile:+-f $makefile} "${flagsArray[@]}"; + unset flagsArray; + fi; + fi; + runHook postInstallCheck +} +installPhase () +{ + + runHook preInstall; + if [[ -z "${makeFlags-}" && -z "${makefile:-}" && ! ( -e Makefile || -e makefile || -e GNUmakefile ) ]]; then + echo "no Makefile or custom installPhase, doing nothing"; + runHook postInstall; + return; + else + foundMakefile=1; + fi; + if [ -n "$prefix" ]; then + mkdir -p "$prefix"; + fi; + local flagsArray=(${enableParallelInstalling:+-j${NIX_BUILD_CORES}} SHELL="$SHELL"); + concatTo flagsArray makeFlags makeFlagsArray installFlags installFlagsArray installTargets=install; + echoCmd 'install flags' "${flagsArray[@]}"; + make ${makefile:+-f $makefile} "${flagsArray[@]}"; + unset flagsArray; + runHook postInstall +} +isELF () +{ + + local fn="$1"; + local fd; + local magic; + exec {fd}< "$fn"; + LANG=C read -r -n 4 -u "$fd" magic; + exec {fd}>&-; + if [ "$magic" = 'ELF' ]; then + return 0; + else + return 1; + fi +} +isMachO () +{ + + local fn="$1"; + local fd; + local magic; + exec {fd}< "$fn"; + LANG=C read -r -n 4 -u "$fd" magic; + exec {fd}>&-; + if [[ "$magic" = $(echo -ne "\xfe\xed\xfa\xcf") || "$magic" = $(echo -ne "\xcf\xfa\xed\xfe") ]]; then + return 0; + else + if [[ "$magic" = $(echo -ne "\xfe\xed\xfa\xce") || "$magic" = $(echo -ne "\xce\xfa\xed\xfe") ]]; then + return 0; + else + if [[ "$magic" = $(echo -ne "\xca\xfe\xba\xbe") || "$magic" = $(echo -ne "\xbe\xba\xfe\xca") ]]; then + return 0; + else + return 1; + fi; + fi; + fi +} +isScript () +{ + + local fn="$1"; + local fd; + local magic; + exec {fd}< "$fn"; + LANG=C read -r -n 2 -u "$fd" magic; + exec {fd}>&-; + if [[ "$magic" =~ \#! ]]; then + return 0; + else + return 1; + fi +} +justBuildPhase () +{ + + runHook preBuild; + local flagsArray=(); + concatTo flagsArray justFlags justFlagsArray; + echoCmd 'build flags' "${flagsArray[@]}"; + just "${flagsArray[@]}"; + runHook postBuild +} +justCheckPhase () +{ + + runHook preCheck; + if [ -z "${checkTarget:-}" ]; then + if just -n test > /dev/null 2>&1; then + checkTarget="test"; + fi; + fi; + if [ -z "${checkTarget:-}" ]; then + echo "no test target found in just, doing nothing"; + else + local flagsArray=(); + concatTo flagsArray justFlags justFlagsArray checkTarget; + echoCmd 'check flags' "${flagsArray[@]}"; + just "${flagsArray[@]}"; + fi; + runHook postCheck +} +justInstallPhase () +{ + + runHook preInstall; + local flagsArray=(); + concatTo flagsArray justFlags justFlagsArray installTargets=install; + echoCmd 'install flags' "${flagsArray[@]}"; + just "${flagsArray[@]}"; + runHook postInstall +} +mapOffset () +{ + + local -r inputOffset="$1"; + local -n outputOffset="$2"; + if (( inputOffset <= 0 )); then + outputOffset=$((inputOffset + hostOffset)); + else + outputOffset=$((inputOffset - 1 + targetOffset)); + fi +} +moveToOutput () +{ + + local patt="$1"; + local dstOut="$2"; + local output; + for output in $(getAllOutputNames); + do + if [ "${!output}" = "$dstOut" ]; then + continue; + fi; + local srcPath; + for srcPath in "${!output}"/$patt; + do + if [ ! -e "$srcPath" ] && [ ! -L "$srcPath" ]; then + continue; + fi; + if [ "$dstOut" = REMOVE ]; then + echo "Removing $srcPath"; + rm -r "$srcPath"; + else + local dstPath="$dstOut${srcPath#${!output}}"; + echo "Moving $srcPath to $dstPath"; + if [ -d "$dstPath" ] && [ -d "$srcPath" ]; then + rmdir "$srcPath" --ignore-fail-on-non-empty; + if [ -d "$srcPath" ]; then + mv -t "$dstPath" "$srcPath"/*; + rmdir "$srcPath"; + fi; + else + mkdir -p "$(readlink -m "$dstPath/..")"; + mv "$srcPath" "$dstPath"; + fi; + fi; + local srcParent="$(readlink -m "$srcPath/..")"; + if [ -n "$(find "$srcParent" -maxdepth 0 -type d -empty 2> /dev/null)" ]; then + echo "Removing empty $srcParent/ and (possibly) its parents"; + rmdir -p --ignore-fail-on-non-empty "$srcParent" 2> /dev/null || true; + fi; + done; + done +} +nixChattyLog () +{ + + _nixLogWithLevel 5 "$*" +} +nixDebugLog () +{ + + _nixLogWithLevel 6 "$*" +} +nixErrorLog () +{ + + _nixLogWithLevel 0 "$*" +} +nixInfoLog () +{ + + _nixLogWithLevel 3 "$*" +} +nixLog () +{ + + [[ -z ${NIX_LOG_FD-} ]] && return 0; + local callerName="${FUNCNAME[1]}"; + if [[ $callerName == "_callImplicitHook" ]]; then + callerName="${hookName:?}"; + fi; + printf "%s: %s\n" "$callerName" "$*" >&"$NIX_LOG_FD" +} +nixNoticeLog () +{ + + _nixLogWithLevel 2 "$*" +} +nixTalkativeLog () +{ + + _nixLogWithLevel 4 "$*" +} +nixVomitLog () +{ + + _nixLogWithLevel 7 "$*" +} +nixWarnLog () +{ + + _nixLogWithLevel 1 "$*" +} +noBrokenSymlinks () +{ + + local -r output="${1:?}"; + local path; + local pathParent; + local symlinkTarget; + local -i numDanglingSymlinks=0; + local -i numReflexiveSymlinks=0; + local -i numUnreadableSymlinks=0; + if [[ ! -e $output ]]; then + nixWarnLog "skipping non-existent output $output"; + return 0; + fi; + nixInfoLog "running on $output"; + while IFS= read -r -d '' path; do + pathParent="$(dirname "$path")"; + if ! symlinkTarget="$(readlink "$path")"; then + nixErrorLog "the symlink $path is unreadable"; + numUnreadableSymlinks+=1; + continue; + fi; + if [[ $symlinkTarget == /* ]]; then + nixInfoLog "symlink $path points to absolute target $symlinkTarget"; + else + nixInfoLog "symlink $path points to relative target $symlinkTarget"; + symlinkTarget="$(realpath --no-symlinks --canonicalize-missing "$pathParent/$symlinkTarget")"; + fi; + if [[ $symlinkTarget = "$TMPDIR"/* ]]; then + nixErrorLog "the symlink $path points to $TMPDIR directory: $symlinkTarget"; + numDanglingSymlinks+=1; + continue; + fi; + if [[ $symlinkTarget != "$NIX_STORE"/* ]]; then + nixInfoLog "symlink $path points outside the Nix store; ignoring"; + continue; + fi; + if [[ $path == "$symlinkTarget" ]]; then + nixErrorLog "the symlink $path is reflexive"; + numReflexiveSymlinks+=1; + else + if [[ ! -e $symlinkTarget ]]; then + nixErrorLog "the symlink $path points to a missing target: $symlinkTarget"; + numDanglingSymlinks+=1; + else + nixDebugLog "the symlink $path is irreflexive and points to a target which exists"; + fi; + fi; + done < <(find "$output" -type l -print0); + if ((numDanglingSymlinks > 0 || numReflexiveSymlinks > 0 || numUnreadableSymlinks > 0)); then + nixErrorLog "found $numDanglingSymlinks dangling symlinks, $numReflexiveSymlinks reflexive symlinks and $numUnreadableSymlinks unreadable symlinks"; + exit 1; + fi; + return 0 +} +noBrokenSymlinksInAllOutputs () +{ + + if [[ -z ${dontCheckForBrokenSymlinks-} ]]; then + for output in $(getAllOutputNames); + do + noBrokenSymlinks "${!output}"; + done; + fi +} +patchELF () +{ + + local dir="$1"; + [ -e "$dir" ] || return 0; + echo "shrinking RPATHs of ELF executables and libraries in $dir"; + local i; + while IFS= read -r -d '' i; do + if [[ "$i" =~ .build-id ]]; then + continue; + fi; + if ! isELF "$i"; then + continue; + fi; + echo "shrinking $i"; + patchelf --shrink-rpath "$i" || true; + done < <(find "$dir" -type f -print0) +} +patchPhase () +{ + + runHook prePatch; + local -a patchesArray; + concatTo patchesArray patches; + local -a flagsArray; + concatTo flagsArray patchFlags=-p1; + for i in "${patchesArray[@]}"; + do + echo "applying patch $i"; + local uncompress=cat; + case "$i" in + *.gz) + uncompress="gzip -d" + ;; + *.bz2) + uncompress="bzip2 -d" + ;; + *.xz) + uncompress="xz -d" + ;; + *.lzma) + uncompress="lzma -d" + ;; + esac; + $uncompress < "$i" 2>&1 | patch "${flagsArray[@]}"; + done; + runHook postPatch +} +patchShebangs () +{ + + local pathName; + local update=false; + while [[ $# -gt 0 ]]; do + case "$1" in + --host) + pathName=HOST_PATH; + shift + ;; + --build) + pathName=PATH; + shift + ;; + --update) + update=true; + shift + ;; + --) + shift; + break + ;; + -* | --*) + echo "Unknown option $1 supplied to patchShebangs" 1>&2; + return 1 + ;; + *) + break + ;; + esac; + done; + echo "patching script interpreter paths in $@"; + local f; + local oldPath; + local newPath; + local arg0; + local args; + local oldInterpreterLine; + local newInterpreterLine; + if [[ $# -eq 0 ]]; then + echo "No arguments supplied to patchShebangs" 1>&2; + return 0; + fi; + local f; + while IFS= read -r -d '' f; do + isScript "$f" || continue; + read -r oldInterpreterLine < "$f" || [ "$oldInterpreterLine" ]; + read -r oldPath arg0 args <<< "${oldInterpreterLine:2}"; + if [[ -z "${pathName:-}" ]]; then + if [[ -n $strictDeps && $f == "$NIX_STORE"* ]]; then + pathName=HOST_PATH; + else + pathName=PATH; + fi; + fi; + if [[ "$oldPath" == *"/bin/env" ]]; then + if [[ $arg0 == "-S" ]]; then + arg0=${args%% *}; + [[ "$args" == *" "* ]] && args=${args#* } || args=; + newPath="$(PATH="${!pathName}" type -P "env" || true)"; + args="-S $(PATH="${!pathName}" type -P "$arg0" || true) $args"; + else + if [[ $arg0 == "-"* || $arg0 == *"="* ]]; then + echo "$f: unsupported interpreter directive \"$oldInterpreterLine\" (set dontPatchShebangs=1 and handle shebang patching yourself)" 1>&2; + exit 1; + else + newPath="$(PATH="${!pathName}" type -P "$arg0" || true)"; + fi; + fi; + else + if [[ -z $oldPath ]]; then + oldPath="/bin/sh"; + fi; + newPath="$(PATH="${!pathName}" type -P "$(basename "$oldPath")" || true)"; + args="$arg0 $args"; + fi; + newInterpreterLine="$newPath $args"; + newInterpreterLine=${newInterpreterLine%${newInterpreterLine##*[![:space:]]}}; + if [[ -n "$oldPath" && ( "$update" == true || "${oldPath:0:${#NIX_STORE}}" != "$NIX_STORE" ) ]]; then + if [[ -n "$newPath" && "$newPath" != "$oldPath" ]]; then + echo "$f: interpreter directive changed from \"$oldInterpreterLine\" to \"$newInterpreterLine\""; + escapedInterpreterLine=${newInterpreterLine//\\/\\\\}; + timestamp=$(stat --printf "%y" "$f"); + tmpFile=$(mktemp -t patchShebangs.XXXXXXXXXX); + sed -e "1 s|.*|#\!$escapedInterpreterLine|" "$f" > "$tmpFile"; + local restoreReadOnly; + if [[ ! -w "$f" ]]; then + chmod +w "$f"; + restoreReadOnly=true; + fi; + cat "$tmpFile" > "$f"; + rm "$tmpFile"; + if [[ -n "${restoreReadOnly:-}" ]]; then + chmod -w "$f"; + fi; + touch --date "$timestamp" "$f"; + fi; + fi; + done < <(find "$@" -type f -perm -0100 -print0) +} +patchShebangsAuto () +{ + + if [[ -z "${dontPatchShebangs-}" && -e "$prefix" ]]; then + if [[ "$output" != out && "$output" = "$outputDev" ]]; then + patchShebangs --build "$prefix"; + else + patchShebangs --host "$prefix"; + fi; + fi +} +prependToVar () +{ + + local -n nameref="$1"; + local useArray type; + if [ -n "$__structuredAttrs" ]; then + useArray=true; + else + useArray=false; + fi; + if type=$(declare -p "$1" 2> /dev/null); then + case "${type#* }" in + -A*) + echo "prependToVar(): ERROR: trying to use prependToVar on an associative array." 1>&2; + return 1 + ;; + -a*) + useArray=true + ;; + *) + useArray=false + ;; + esac; + fi; + shift; + if $useArray; then + nameref=("$@" ${nameref+"${nameref[@]}"}); + else + nameref="$* ${nameref-}"; + fi +} +printLines () +{ + + (( "$#" > 0 )) || return 0; + printf '%s\n' "$@" +} +printPhases () +{ + + definePhases; + local phase; + for phase in ${phases[*]}; + do + printf '%s\n' "$phase"; + done +} +printWords () +{ + + (( "$#" > 0 )) || return 0; + printf '%s ' "$@" +} +recordPropagatedDependencies () +{ + + declare -ra flatVars=(depsBuildBuildPropagated propagatedNativeBuildInputs depsBuildTargetPropagated depsHostHostPropagated propagatedBuildInputs depsTargetTargetPropagated); + declare -ra flatFiles=("${propagatedBuildDepFiles[@]}" "${propagatedHostDepFiles[@]}" "${propagatedTargetDepFiles[@]}"); + local propagatedInputsIndex; + for propagatedInputsIndex in "${!flatVars[@]}"; + do + local propagatedInputsSlice="${flatVars[$propagatedInputsIndex]}[@]"; + local propagatedInputsFile="${flatFiles[$propagatedInputsIndex]}"; + [[ -n "${!propagatedInputsSlice}" ]] || continue; + mkdir -p "${!outputDev}/nix-support"; + printWords ${!propagatedInputsSlice} > "${!outputDev}/nix-support/$propagatedInputsFile"; + done +} +runHook () +{ + + local hookName="$1"; + shift; + local hooksSlice="${hookName%Hook}Hooks[@]"; + local hook; + for hook in "_callImplicitHook 0 $hookName" ${!hooksSlice+"${!hooksSlice}"}; + do + _logHook "$hookName" "$hook" "$@"; + _eval "$hook" "$@"; + done; + return 0 +} +runOneHook () +{ + + local hookName="$1"; + shift; + local hooksSlice="${hookName%Hook}Hooks[@]"; + local hook ret=1; + for hook in "_callImplicitHook 1 $hookName" ${!hooksSlice+"${!hooksSlice}"}; + do + _logHook "$hookName" "$hook" "$@"; + if _eval "$hook" "$@"; then + ret=0; + break; + fi; + done; + return "$ret" +} +runPhase () +{ + + local curPhase="$*"; + if [[ "$curPhase" = unpackPhase && -n "${dontUnpack:-}" ]]; then + return; + fi; + if [[ "$curPhase" = patchPhase && -n "${dontPatch:-}" ]]; then + return; + fi; + if [[ "$curPhase" = configurePhase && -n "${dontConfigure:-}" ]]; then + return; + fi; + if [[ "$curPhase" = buildPhase && -n "${dontBuild:-}" ]]; then + return; + fi; + if [[ "$curPhase" = checkPhase && -z "${doCheck:-}" ]]; then + return; + fi; + if [[ "$curPhase" = installPhase && -n "${dontInstall:-}" ]]; then + return; + fi; + if [[ "$curPhase" = fixupPhase && -n "${dontFixup:-}" ]]; then + return; + fi; + if [[ "$curPhase" = installCheckPhase && -z "${doInstallCheck:-}" ]]; then + return; + fi; + if [[ "$curPhase" = distPhase && -z "${doDist:-}" ]]; then + return; + fi; + showPhaseHeader "$curPhase"; + dumpVars; + local startTime endTime; + startTime=$(date +"%s"); + eval "${!curPhase:-$curPhase}"; + endTime=$(date +"%s"); + showPhaseFooter "$curPhase" "$startTime" "$endTime"; + if [ "$curPhase" = unpackPhase ]; then + [ -n "${sourceRoot:-}" ] && chmod +x -- "${sourceRoot}"; + cd -- "${sourceRoot:-.}"; + fi +} +showPhaseFooter () +{ + + local phase="$1"; + local startTime="$2"; + local endTime="$3"; + local delta=$(( endTime - startTime )); + (( delta < 30 )) && return; + local H=$((delta/3600)); + local M=$((delta%3600/60)); + local S=$((delta%60)); + echo -n "$phase completed in "; + (( H > 0 )) && echo -n "$H hours "; + (( M > 0 )) && echo -n "$M minutes "; + echo "$S seconds" +} +showPhaseHeader () +{ + + local phase="$1"; + echo "Running phase: $phase"; + if [[ -z ${NIX_LOG_FD-} ]]; then + return; + fi; + printf "@nix { \"action\": \"setPhase\", \"phase\": \"%s\" }\n" "$phase" >&"$NIX_LOG_FD" +} +stripDirs () +{ + + local cmd="$1"; + local ranlibCmd="$2"; + local paths="$3"; + local stripFlags="$4"; + local excludeFlags=(); + local pathsNew=; + [ -z "$cmd" ] && echo "stripDirs: Strip command is empty" 1>&2 && exit 1; + [ -z "$ranlibCmd" ] && echo "stripDirs: Ranlib command is empty" 1>&2 && exit 1; + local pattern; + if [ -n "${stripExclude:-}" ]; then + for pattern in "${stripExclude[@]}"; + do + excludeFlags+=(-a '!' '(' -name "$pattern" -o -wholename "$prefix/$pattern" ')'); + done; + fi; + local p; + for p in ${paths}; + do + if [ -e "$prefix/$p" ]; then + pathsNew="${pathsNew} $prefix/$p"; + fi; + done; + paths=${pathsNew}; + if [ -n "${paths}" ]; then + echo "stripping (with command $cmd and flags $stripFlags) in $paths"; + local striperr; + striperr="$(mktemp --tmpdir="$TMPDIR" 'striperr.XXXXXX')"; + find $paths -type f "${excludeFlags[@]}" -a '!' -path "$prefix/lib/debug/*" -printf '%D-%i,%p\0' | sort -t, -k1,1 -u -z | cut -d, -f2- -z | xargs -r -0 -n1 -P "$NIX_BUILD_CORES" -- $cmd $stripFlags 2> "$striperr" || exit_code=$?; + [[ "$exit_code" = 123 || -z "$exit_code" ]] || ( cat "$striperr" 1>&2 && exit 1 ); + rm "$striperr"; + find $paths -name '*.a' -type f -exec $ranlibCmd '{}' \; 2> /dev/null; + fi +} +stripHash () +{ + + local strippedName casematchOpt=0; + strippedName="$(basename -- "$1")"; + shopt -q nocasematch && casematchOpt=1; + shopt -u nocasematch; + if [[ "$strippedName" =~ ^[a-z0-9]{32}- ]]; then + echo "${strippedName:33}"; + else + echo "$strippedName"; + fi; + if (( casematchOpt )); then + shopt -s nocasematch; + fi +} +substitute () +{ + + local input="$1"; + local output="$2"; + shift 2; + if [ ! -f "$input" ]; then + echo "substitute(): ERROR: file '$input' does not exist" 1>&2; + return 1; + fi; + local content; + consumeEntire content < "$input"; + if [ -e "$output" ]; then + chmod +w "$output"; + fi; + substituteStream content "file '$input'" "$@" > "$output" +} +substituteAll () +{ + + local input="$1"; + local output="$2"; + local -a args=(); + _allFlags; + substitute "$input" "$output" "${args[@]}" +} +substituteAllInPlace () +{ + + local fileName="$1"; + shift; + substituteAll "$fileName" "$fileName" "$@" +} +substituteAllStream () +{ + + local -a args=(); + _allFlags; + substituteStream "$1" "$2" "${args[@]}" +} +substituteInPlace () +{ + + local -a fileNames=(); + for arg in "$@"; + do + if [[ "$arg" = "--"* ]]; then + break; + fi; + fileNames+=("$arg"); + shift; + done; + if ! [[ "${#fileNames[@]}" -gt 0 ]]; then + echo "substituteInPlace called without any files to operate on (files must come before options!)" 1>&2; + return 1; + fi; + for file in "${fileNames[@]}"; + do + substitute "$file" "$file" "$@"; + done +} +substituteStream () +{ + + local var=$1; + local description=$2; + shift 2; + while (( "$#" )); do + local replace_mode="$1"; + case "$1" in + --replace) + if ! "$_substituteStream_has_warned_replace_deprecation"; then + echo "substituteStream() in derivation $name: WARNING: '--replace' is deprecated, use --replace-{fail,warn,quiet}. ($description)" 1>&2; + _substituteStream_has_warned_replace_deprecation=true; + fi; + replace_mode='--replace-warn' + ;& + --replace-quiet | --replace-warn | --replace-fail) + pattern="$2"; + replacement="$3"; + shift 3; + if ! [[ "${!var}" == *"$pattern"* ]]; then + if [ "$replace_mode" == --replace-warn ]; then + printf "substituteStream() in derivation $name: WARNING: pattern %q doesn't match anything in %s\n" "$pattern" "$description" 1>&2; + else + if [ "$replace_mode" == --replace-fail ]; then + printf "substituteStream() in derivation $name: ERROR: pattern %q doesn't match anything in %s\n" "$pattern" "$description" 1>&2; + return 1; + fi; + fi; + fi; + eval "$var"'=${'"$var"'//"$pattern"/"$replacement"}' + ;; + --subst-var) + local varName="$2"; + shift 2; + if ! [[ "$varName" =~ ^[a-zA-Z_][a-zA-Z0-9_]*$ ]]; then + echo "substituteStream() in derivation $name: ERROR: substitution variables must be valid Bash names, \"$varName\" isn't." 1>&2; + return 1; + fi; + if [ -z ${!varName+x} ]; then + echo "substituteStream() in derivation $name: ERROR: variable \$$varName is unset" 1>&2; + return 1; + fi; + pattern="@$varName@"; + replacement="${!varName}"; + eval "$var"'=${'"$var"'//"$pattern"/"$replacement"}' + ;; + --subst-var-by) + pattern="@$2@"; + replacement="$3"; + eval "$var"'=${'"$var"'//"$pattern"/"$replacement"}'; + shift 3 + ;; + *) + echo "substituteStream() in derivation $name: ERROR: Invalid command line argument: $1" 1>&2; + return 1 + ;; + esac; + done; + printf "%s" "${!var}" +} +unpackFile () +{ + + curSrc="$1"; + echo "unpacking source archive $curSrc"; + if ! runOneHook unpackCmd "$curSrc"; then + echo "do not know how to unpack source archive $curSrc"; + exit 1; + fi +} +unpackPhase () +{ + + runHook preUnpack; + if [ -z "${srcs:-}" ]; then + if [ -z "${src:-}" ]; then + echo 'variable $src or $srcs should point to the source'; + exit 1; + fi; + srcs="$src"; + fi; + local -a srcsArray; + concatTo srcsArray srcs; + local dirsBefore=""; + for i in *; + do + if [ -d "$i" ]; then + dirsBefore="$dirsBefore $i "; + fi; + done; + for i in "${srcsArray[@]}"; + do + unpackFile "$i"; + done; + : "${sourceRoot=}"; + if [ -n "${setSourceRoot:-}" ]; then + runOneHook setSourceRoot; + else + if [ -z "$sourceRoot" ]; then + for i in *; + do + if [ -d "$i" ]; then + case $dirsBefore in + *\ $i\ *) + + ;; + *) + if [ -n "$sourceRoot" ]; then + echo "unpacker produced multiple directories"; + exit 1; + fi; + sourceRoot="$i" + ;; + esac; + fi; + done; + fi; + fi; + if [ -z "$sourceRoot" ]; then + echo "unpacker appears to have produced no directories"; + exit 1; + fi; + echo "source root is $sourceRoot"; + if [ "${dontMakeSourcesWritable:-0}" != 1 ]; then + chmod -R u+w -- "$sourceRoot"; + fi; + runHook postUnpack +} +updateAutotoolsGnuConfigScriptsPhase () +{ + + if [ -n "${dontUpdateAutotoolsGnuConfigScripts-}" ]; then + return; + fi; + for script in config.sub config.guess; + do + for f in $(find . -type f -name "$script"); + do + echo "Updating Autotools / GNU config script to a newer upstream version: $f"; + cp -f "/nix/store/dikzgw0kinvli9fdhv7qxqbbmirilbn3-gnu-config-2024-01-01/$script" "$f"; + done; + done +} +updateSourceDateEpoch () +{ + + local path="$1"; + [[ $path == -* ]] && path="./$path"; + local -a res=($(find "$path" -type f -not -newer "$NIX_BUILD_TOP/.." -printf '%T@ "%p"\0' | sort -n --zero-terminated | tail -n1 --zero-terminated | head -c -1)); + local time="${res[0]//\.[0-9]*/}"; + local newestFile="${res[1]}"; + if [ "${time:-0}" -gt "$SOURCE_DATE_EPOCH" ]; then + echo "setting SOURCE_DATE_EPOCH to timestamp $time of file $newestFile"; + export SOURCE_DATE_EPOCH="$time"; + local now="$(date +%s)"; + if [ "$time" -gt $((now - 60)) ]; then + echo "warning: file $newestFile may be generated; SOURCE_DATE_EPOCH may be non-deterministic"; + fi; + fi +} +PATH="$PATH${nix_saved_PATH:+:$nix_saved_PATH}" +XDG_DATA_DIRS="$XDG_DATA_DIRS${nix_saved_XDG_DATA_DIRS:+:$nix_saved_XDG_DATA_DIRS}" +export NIX_BUILD_TOP="$(mktemp -d -t nix-shell.XXXXXX)" +export TMP="$NIX_BUILD_TOP" +export TMPDIR="$NIX_BUILD_TOP" +export TEMP="$NIX_BUILD_TOP" +export TEMPDIR="$NIX_BUILD_TOP" +eval "${shellHook:-}" diff --git a/.dockerignore b/.dockerignore index 25735115..c047e816 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,5 +1,10 @@ node_modules .next +.turbo +out +target +apps/desktop +rust .git .gitignore *.md diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 5edc56fb..504c2f78 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -1,152 +1,40 @@ # Contributing to OpenCut -⚠️ We are currently NOT accepting feature PRs while we build out the core editor. - -If you want to contribute: - -1. Open an issue first to discuss -2. Wait for maintainer approval -3. Only then start coding - -Critical bug fixes may be accepted on a case-by-case basis. - Thank you for your interest in contributing to OpenCut! This document provides guidelines and instructions for contributing. ## Getting Started ### Prerequisites -- [Node.js](https://nodejs.org/en/) (v18 or later) - [Bun](https://bun.sh/docs/installation) - (for `npm` alternative) -- [Docker](https://docs.docker.com/get-docker/) and [Docker Compose](https://docs.docker.com/compose/install/) -- Rust toolchain (only needed for `apps/desktop`) +- Rust toolchain (only needed when editing `rust/`) -> **Note:** Docker is optional, but it's essential for running the local database and Redis services. If you're planning to contribute to frontend features, you can skip the Docker setup. If you have followed the steps below in [Setup](#setup), you're all set to go! +No env files, no database, no Docker — the editor is fully client-side; all state lives in IndexedDB. ### Setup 1. Fork the repository 2. Clone your fork locally -3. Navigate to the web app directory: `cd apps/web` -4. Copy `.env.example` to `.env.local`: - - ```bash - # Unix/Linux/Mac - cp .env.example .env.local +3. Install dependencies: `bun install` +4. Start the development server from the repo root: `bun dev:web` - # Windows Command Prompt - copy .env.example .env.local - - # Windows PowerShell - Copy-Item .env.example .env.local - ``` - -5. Install dependencies: `bun install` -6. Start the development server: `bun run dev` +The editor is at [http://localhost:3000](http://localhost:3000). > **Note:** Web development uses the published `opencut-wasm` package by default, so a fresh clone does not need a local WASM build. > > If you are editing `rust/wasm`, run `bun run build:wasm`, then `cd rust/wasm/pkg && bun link`, then `cd ../../../apps/web && bun link opencut-wasm`. -### Desktop setup - -Only needed if you're working on `apps/desktop`. See [`apps/desktop/README.md`](../apps/desktop/README.md) — it's a two-step process: Rust toolchain first via `script/setup-rust`, then desktop native dependencies via `apps/desktop/script/setup`. - -## What to Focus On - -**🎯 Good Areas to Contribute:** - -- Timeline functionality and UI improvements -- Project management features -- Performance optimizations -- Bug fixes in existing functionality -- UI/UX improvements -- Documentation and testing - -**⚠️ Areas to Avoid:** - -- Preview panel enhancements (text fonts, stickers, effects) -- Export functionality improvements -- Preview rendering optimizations - -**Why?** We're currently planning a major refactor of the preview system. The current preview renders DOM elements (HTML), but we're moving to a binary rendering approach similar to CapCut. This new system will ensure consistency between preview and export, and provide much better performance and quality. - -The current HTML-based preview is essentially a prototype - the binary approach will be the "real deal." To avoid wasted effort, please focus on other areas of the application until this refactor is complete. - -If you're unsure whether your idea falls into the preview category, feel free to ask us [directly in discord](https://discord.gg/zmR9N35cjK) or create a GitHub issue! - -## Development Setup - -### Local Development - -1. Start the database and Redis services: - - ```bash - # From project root - docker-compose up -d - ``` - -2. Navigate to the web app directory: - - ```bash - cd apps/web - ``` - -3. Copy `.env.example` to `.env.local`: - - ```bash - # Unix/Linux/Mac - cp .env.example .env.local - - # Windows Command Prompt - copy .env.example .env.local - - # Windows PowerShell - Copy-Item .env.example .env.local - ``` - -4. Configure required environment variables in `.env.local`: - - **Required Variables:** - - ```bash - # Database (matches docker-compose.yaml) - DATABASE_URL="postgresql://opencut:opencut@localhost:5432/opencut" - - # Generate a secure secret for Better Auth - BETTER_AUTH_SECRET="your-generated-secret-here" - NEXT_PUBLIC_SITE_URL="http://localhost:3000" - - # Redis (matches docker-compose.yaml) - UPSTASH_REDIS_REST_URL="http://localhost:8079" - UPSTASH_REDIS_REST_TOKEN="example_token" - - # Development - NODE_ENV="development" - ``` - - **Generate BETTER_AUTH_SECRET:** - - ```bash - # Unix/Linux/Mac - openssl rand -base64 32 - - # Windows PowerShell (simple method) - [System.Web.Security.Membership]::GeneratePassword(32, 0) - - # Cross-platform (using Node.js) - node -e "console.log(require('crypto').randomBytes(32).toString('base64'))" - - # Or use an online generator: https://generate-secret.vercel.app/32 - ``` +### Desktop -5. Run database migrations: `bun run db:migrate` -6. Start the development server: `bun run dev` +Only needed if you're working on `apps/desktop` (Electron shell): -### Desktop +```bash +bun build:web # static export -> apps/web/out +bun run --cwd apps/desktop copy:web # bundle it into the desktop app +bun run --cwd apps/desktop dev # run the shell +``` -Working on `apps/desktop`? See [`apps/desktop/README.md`](../apps/desktop/README.md) for setup. Web-only contributors can ignore this entirely. +`bun run --cwd apps/desktop dist` builds AppImage, `.deb`, and `.rpm` into `apps/desktop/release/`. ## How to Contribute @@ -168,15 +56,15 @@ Working on `apps/desktop`? See [`apps/desktop/README.md`](../apps/desktop/README 2. Make your changes 3. Run the relevant checks for the area you touched: - - Web changes: from `apps/web`, run `bun run lint` and `bun run format` - - Desktop changes: run `./apps/desktop/script/setup` if your environment isn't set up yet + - Web changes: from `apps/web`, run `bun run lint` and `bun run format`; from the repo root, `bun test` and `bunx tsc --noEmit --project apps/web` + - Desktop changes: `bun run --cwd apps/desktop build` 4. Commit your changes with a descriptive message 5. Push to your fork and create a pull request ## Code Style -- We use ESLint for linting and Prettier for formatting +- We use ESLint for linting and Prettier for formatting (tabs) - Run `bun run format` from the `apps/web` directory to format code - Run `bun run lint` from the `apps/web` directory to check for linting issues - Follow the existing code patterns diff --git a/.github/workflows/bun-ci.yml b/.github/workflows/bun-ci.yml index 8f702f72..ed79742f 100644 --- a/.github/workflows/bun-ci.yml +++ b/.github/workflows/bun-ci.yml @@ -21,17 +21,6 @@ jobs: matrix: os: [ubuntu-latest, windows-latest, macos-latest] - env: - DATABASE_URL: "postgresql://opencut:opencut@localhost:5432/opencut" - BETTER_AUTH_SECRET: "supersecret" - NEXT_PUBLIC_SITE_URL: "http://localhost:3000" - UPSTASH_REDIS_REST_URL: "https://your-upstash-redis-url" - UPSTASH_REDIS_REST_TOKEN: "your-upstash-redis-token" - NEXT_PUBLIC_MARBLE_API_URL: "https://placeholder.example.com" - MARBLE_WORKSPACE_KEY: "placeholder" - FREESOUND_CLIENT_ID: "placeholder" - FREESOUND_API_KEY: "placeholder" - steps: - name: Checkout repository uses: actions/checkout@v4 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..0469fc3a --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,56 @@ +name: Release + +on: + push: + tags: + - "v*" + +permissions: + contents: write + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Install Bun + uses: oven-sh/setup-bun@735343b667d3e6f658f44d0eca948eb6282f2b76 + with: + bun-version: 1.2.18 + + - name: Cache Bun modules + uses: actions/cache@v4 + with: + path: ~/.bun/install/cache + key: ${{ runner.os }}-bun-1.2.18-${{ hashFiles('bun.lock') }} + + - name: Install dependencies + run: bun install --frozen-lockfile + + - name: Build web app (static export) + run: bun build:web + + - name: Package static web bundle + working-directory: apps/web + run: tar -czf ../../opencut-web-static.tar.gz -C out . + + - name: Copy web bundle into desktop app + working-directory: apps/desktop + run: bun run copy:web + + - name: Build desktop packages (AppImage, deb, rpm) + working-directory: apps/desktop + run: bun run dist + + - name: Upload release assets + uses: softprops/action-gh-release@v2 + with: + generate_release_notes: true + files: | + opencut-web-static.tar.gz + apps/desktop/release/OpenCut-*-linux-*.AppImage + apps/desktop/release/OpenCut-*-linux-*.deb + apps/desktop/release/OpenCut-*-linux-*.rpm diff --git a/AGENTS.md b/AGENTS.md index 12d2efd1..6c61e44e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -10,7 +10,7 @@ Bun + Turborepo monorepo with three pillars: 1. `apps/web` — Next.js 16 (App Router, Turbopack dev) React 19 editor. Consumes the Rust core as the npm dependency `opencut-wasm`. 2. `rust/` — Cargo workspace (root `Cargo.toml`) of 6 shared crates (`time`, `bridge`, `effects`, `gpu`, `masks`, `compositor`; wgpu + WGSL shaders) plus `rust/wasm`, the cdylib compiled by wasm-pack into `rust/wasm/pkg` and published as `opencut-wasm`. Business logic is actively migrating from TypeScript to here. -3. `apps/desktop` — GPUI 0.2.2 native app, currently a hello-world scaffold window. Ignore unless explicitly working on it. +3. `apps/desktop` — Electron shell for Linux. Serves the static web build over a privileged `opencut://` protocol and bridges exports to a bundled ffmpeg (libx264/libvpx-vp9) over IPC (`src/export.ts`). The renderer-side native exporter lives in `apps/web/src/services/renderer/native-exporter.ts` and activates automatically when the preload bridge (`window.opencutDesktop`) is present. The web app has a hybrid state architecture: @@ -37,7 +37,7 @@ Typical flow (split at playhead, key "s"): keybindings-store → `invokeAction(" | `apps/web/src/project/transfer/` | Single-file `.ocp` import/export (fflate zip: project.json + media + manifest) | | `apps/web/src/features.ts` | `FEATURES` flags hiding unfinished upstream domains (sounds/stickers/effects) from the UI | | `apps/web/src/components/` | Shared React: `ui/` (shadcn-style radix), `editor/`, `providers/` | -| `apps/web/src/app/` | App Router pages + `api/` routes | +| `apps/web/src/app/` | App Router pages (static export) | | `rust/crates/` | Shared Rust crates; `rust/wasm/` compiles them to the `opencut-wasm` npm package | | `docs/` | Subsystem docs: `actions.md`, `keyframes.md`, `effects-renderer.md`, `countries-search.md` | | `notes/` | Design notes (e.g. `primitives-vs-domains.md`) | @@ -50,20 +50,22 @@ Run from repo root unless noted. `flake.nix` provides a devshell (`nix develop`) ```bash bun install -cp apps/web/.env.example apps/web/.env.local # required; zod validates env at import time -docker compose up -d db redis serverless-redis-http # optional; not needed for editor work -bun dev:web # http://localhost:3000 (editor at /editor/) -bun build:web # next build via turbo +bun dev:web # http://localhost:3000 +bun build:web # static export via turbo -> apps/web/out bun test # all JS/TS tests (Bun test runner) bun lint:web # eslint apps/web/src bun lint:web:fix # with --fix ``` -Inside `apps/web`: `bun run dev`, `bun run format` (prettier all of `src`), `bun run db:migrate` / `db:generate` (drizzle-kit), `bun run preview` / `deploy` (OpenNext → Cloudflare Workers). Typecheck with `bunx tsc --noEmit` (no script exists; see Testing & QA for pre-existing errors). +No env files, no database — the backend was deleted; the editor is fully client-side on IndexedDB. -WASM (only when editing `rust/`): `./script/setup-rust` installs rustup + wasm-pack; then `bun build:wasm` (wasm-pack → `rust/wasm/pkg`), `bun dev:wasm` (cargo-watch rebuild), and `bun link` in `rust/wasm/pkg` + `bun link opencut-wasm` in `apps/web` to use the local build (`bun add opencut-wasm` to revert). Rust tests: `cargo test -p `. Desktop: `cargo run -p opencut-desktop` after `apps/desktop/script/setup`. +Inside `apps/web`: `bun run dev`, `bun run format` (prettier all of `src`). Typecheck with `bunx tsc --noEmit` (no script exists; see Testing & QA for pre-existing errors). -Self-hosting: `docker compose up -d` (full stack at http://localhost:3100; postgres 17, redis 7, serverless-redis-http Upstash shim on :8079). +Desktop: `bun build:web` first, then `bun run --cwd apps/desktop copy:web` (bundles `apps/web/out`), `bun run --cwd apps/desktop dev` to run, `bun run --cwd apps/desktop dist` to build AppImage/deb/rpm into `apps/desktop/release/`. Tags `v*` trigger `.github/workflows/release.yml` (static web tarball + Linux packages). + +WASM (only when editing `rust/`): install rustup + wasm-pack + cargo-watch, then `bun build:wasm` (wasm-pack → `rust/wasm/pkg`), `bun dev:wasm` (cargo-watch rebuild), and `bun link` in `rust/wasm/pkg` + `bun link opencut-wasm` in `apps/web` to use the local build (`bun add opencut-wasm` to revert). Rust tests: `cargo test -p `. + +Self-hosting: `docker compose up -d` (nginx serving the static build at http://localhost:3000). ## Code Conventions & Common Patterns @@ -82,7 +84,7 @@ Self-hosting: `docker compose up -d` (full stack at http://localhost:3100; postg | File | Role | |---|---| | `apps/web/src/app/layout.tsx` | Root layout (ThemeProvider, TooltipProvider, Toaster) | -| `apps/web/src/app/editor/[project_id]/page.tsx` | Editor page — resizable panels inside `EditorProvider` | +| `apps/web/src/app/editor/page.tsx` | Editor page (`?project=`) — resizable panels inside `EditorProvider` | | `apps/web/src/components/providers/editor-provider.tsx` | Loads project, initializes GPU renderer, mounts actions + keybindings | | `apps/web/src/core/index.ts` | EditorCore singleton; wires all managers + command reactor | | `apps/web/src/core/managers/commands.ts` | Undo/redo history, selection overrides, reactors, ripple | @@ -90,18 +92,19 @@ Self-hosting: `docker compose up -d` (full stack at http://localhost:3100; postg | `apps/web/src/actions/use-editor-actions.ts` | Action handler implementations | | `apps/web/src/commands/base-command.ts` | `abstract class Command` contract | | `apps/web/src/wasm/media-time.ts` | Branded MediaTime type + validated wasm wrappers | -| `apps/web/src/db/schema.ts` | Drizzle schema (note: `apps/web/drizzle.config.ts` points at the stale path `./src/lib/db/schema.ts`) | +| `apps/web/src/desktop/bridge.ts` | Typed `window.opencutDesktop` bridge detection (Electron preload) | +| `apps/web/src/services/renderer/native-exporter.ts` | Desktop export path: frames → IPC → ffmpeg (mirrors `SceneExporter`) | | `eslint.config.mjs` | Flat ESLint config + inline `eslint-plugin-opencut` | | `turbo.json` | Task graph (build/dev/preview/deploy/lint/format) | -| `apps/web/wrangler.jsonc` + `open-next.config.ts` | Cloudflare Workers deploy target via OpenNext | -| `docker-compose.yml`, `apps/web/Dockerfile` | Local services + self-hosted production image | +| `apps/desktop/electron-builder.yml` | Linux packaging: AppImage/deb/rpm, artifact naming | +| `docker-compose.yml`, `apps/web/Dockerfile` | Self-hosted static build via nginx | ## Runtime/Tooling Preferences - Bun is the package manager and script runner, pinned `1.2.18` via `packageManager` in root and `apps/web/package.json`; bun 1.3.x (from the Nix devshell) also works for install/dev/tsc. Lockfile: root `bun.lock` only. `.npmrc`: `install-strategy=nested`, `node-linker=isolated`. - No `engines` field; Node 24 comes from the devshell but the app is built/run by bun and Next. CI pins bun 1.2.18. -- Rust toolchain (rustup, wasm-pack, cargo-watch) is needed ONLY for `rust/` or `apps/desktop` work — install via `script/setup-rust`. The web app normally uses the published `opencut-wasm` npm package; no local Rust required for editor work. -- Deploy target is Cloudflare Workers via `@opennextjs/cloudflare` (worker name `opencut`); self-hosting alternative is the Docker standalone build. Root `wrangler.jsonc` duplicates the apps/web one — edit `apps/web/wrangler.jsonc`. +- Rust toolchain (rustup, wasm-pack, cargo-watch) is needed ONLY for `rust/` work. The web app normally uses the published `opencut-wasm` npm package; no local Rust required for editor work. +- Distribution: GitHub Releases (tag-triggered) — static web tarball + Linux desktop packages. Self-hosting alternative is the nginx Docker image. - Dead config to ignore: root `build:tools`/`dev:tools`/`start:tools` scripts (target nonexistent `@opencut/tools`), turbo `check-types` task (no package implements it), root `format:web` (formats only `src/services/renderer`; use `bun run format` in apps/web), `packages/` workspace glob (empty). ## Testing & QA diff --git a/Cargo.toml b/Cargo.toml index b00c78e8..3e5527e0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,6 @@ [workspace] resolver = "2" members = [ - "apps/desktop", "rust/crates/time", "rust/crates/bridge", "rust/crates/effects", diff --git a/README.md b/README.md index 95f5efb4..4702f926 100644 --- a/README.md +++ b/README.md @@ -1,162 +1,70 @@ -# OpenCut (Legacy) +# OpenCut -This is the original OpenCut codebase. It's archived and no longer maintained. +Offline-first, privacy-first video editor. Your projects and media never leave your device. -The rewrite is happening at [opencut-app/opencut](https://github.com/opencut-app/opencut). +Continues the abandoned [opencut-app/OpenCut](https://github.com/opencut-app/OpenCut) codebase: the editor was already fully client-side, so the dead backend (auth, database, blog) was ripped out. The web app is now a fully static site, and there is a self-contained Linux desktop app with native ffmpeg exports. -## Sponsors +## Variants -Thanks to [Vercel](https://vercel.com?utm_source=github-opencut&utm_campaign=oss) and [fal.ai](https://fal.ai?utm_source=github-opencut&utm_campaign=oss) for their support of open-source software. +- **Desktop (Linux)**: self-contained app — AppImage, `.deb`, `.rpm` on the [releases page](https://github.com/cashmeredev/opencut/releases). Exports are encoded by a bundled ffmpeg (libx264 / libvpx-vp9) instead of the browser encoder, so MP4 quality settings are actually honored and colors are correctly tagged BT.709. +- **Web**: fully static site, host it anywhere. Grab `opencut-web-static.tar.gz` from releases and serve it with any static file server. - - Vercel OSS Program - +## Project structure - - Powered by fal.ai - +- `apps/web/`: Next.js editor, built as a static export (`next build` → `out/`). All state lives in IndexedDB. +- `apps/desktop/`: Electron shell. Serves the static web build over a privileged `opencut://` protocol and bridges exports to a bundled ffmpeg over IPC. +- `rust/`: wgpu compositor, effects, masks, and time primitives compiled to WASM (`opencut-wasm`). -## Why? +## Development -- **Privacy**: Your videos stay on your device -- **Free features**: Most basic CapCut features are now paywalled -- **Simple**: People want editors that are easy to use - CapCut proved that - -## Project Structure - -- `apps/web/`: Next.js web application -- `apps/desktop/`: Native desktop app built with GPUI (in progress) -- `rust/`: Platform-agnostic core: GPU compositor, effects, masks, and WASM bindings. We're actively migrating business logic here from TypeScript. -- `docs/`: Architecture and subsystem documentation - -## Getting Started - -### Prerequisites - -- [Bun](https://bun.sh/docs/installation) -- [Docker](https://docs.docker.com/get-docker/) and [Docker Compose](https://docs.docker.com/compose/install/) - -> **Note:** Docker is optional but recommended for running the local database and Redis. If you only want to work on frontend features, you can skip it. - -### Setup - -1. Fork and clone the repository - -2. Copy the environment file: - - ```bash - # Unix/Linux/Mac - cp apps/web/.env.example apps/web/.env.local - - # Windows PowerShell - Copy-Item apps/web/.env.example apps/web/.env.local - ``` - -3. Start the database and Redis: - - ```bash - docker compose up -d db redis serverless-redis-http - ``` - -4. Install dependencies and start the dev server: - - ```bash - bun install - bun dev:web - ``` - -The application will be available at [http://localhost:3000](http://localhost:3000). - -The `.env.example` has sensible defaults that match the Docker Compose config — it should work out of the box. - -### Desktop setup - -Desktop is opt-in. If you're only working on the web app, skip this entirely. - -If you want to get ready for `apps/desktop`, see [`apps/desktop/README.md`](apps/desktop/README.md). It's a two-step setup: Rust toolchain first, then desktop native dependencies. - -### Local WASM development - -Only needed if you're editing `rust/wasm` and want the web app to use your local build instead of the published package. - -**Prerequisites** — install these once before anything else: +Prerequisites: [Bun](https://bun.sh/docs/installation). No env files, no database, no Docker. ```bash -# Rust toolchain -curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh - -# build the WASM package -cargo install wasm-pack - -# reruns the build on file changes, used by bun dev:wasm -cargo install cargo-watch +bun install +bun dev:web ``` -1. Build the package once from the repo root: +The editor is at [http://localhost:3000](http://localhost:3000). - ```bash - bun run build:wasm - ``` +### Desktop -2. Register the generated package for linking: - - ```bash - cd rust/wasm/pkg - bun link - ``` - -3. Link `apps/web` to the local package: +```bash +bun build:web # static export -> apps/web/out +bun run --cwd apps/desktop copy:web # bundle it into the desktop app +bun run --cwd apps/desktop dev # run the shell +``` - ```bash - cd apps/web - bun link opencut-wasm - ``` +Package for Linux (AppImage, deb, rpm into `apps/desktop/release/`): -4. Rebuild on changes while you work: +```bash +bun run --cwd apps/desktop dist +``` - ```bash - bun dev:wasm - ``` +### Local WASM development -To switch `apps/web` back to the published package, run: +Only needed if you're editing `rust/wasm` and want the web app to use your local build instead of the published package. Requires a Rust toolchain, `wasm-pack`, and `cargo-watch`: ```bash -cd apps/web -bun add opencut-wasm +bun run build:wasm # build once +cd rust/wasm/pkg && bun link +cd apps/web && bun link opencut-wasm +bun dev:wasm # rebuild on changes ``` -### Self-Hosting with Docker +Switch back to the published package with `cd apps/web && bun add opencut-wasm`. -To run everything (including a production build of the app) in Docker: +### Self-hosting the web app ```bash docker compose up -d ``` -The app will be available at [http://localhost:3100](http://localhost:3100). - -## Contributing - -We welcome contributions! While we're actively developing and refactoring certain areas, there are plenty of opportunities to contribute effectively. - -**🎯 Focus areas:** Timeline functionality, project management, performance, bug fixes, and UI improvements outside the preview panel. +Serves the static build at [http://localhost:3000](http://localhost:3000). Any static file server works — the build output is `apps/web/out/`. -**⚠️ Avoid for now:** Preview panel enhancements (fonts, stickers, effects) and export functionality - we're refactoring these with a new binary rendering approach. +## Releases -See our [Contributing Guide](.github/CONTRIBUTING.md) for detailed setup instructions, development guidelines, and complete focus area guidance. - -**Quick start for contributors:** - -- Fork the repo and clone locally -- Follow the setup instructions in CONTRIBUTING.md -- Working on `apps/desktop`? See [`apps/desktop/README.md`](apps/desktop/README.md) for setup -- Create a feature branch and submit a PR +Tags (`v*`) trigger the GitHub release workflow: static web tarball plus AppImage, `.deb`, and `.rpm`. ## License [MIT LICENSE](LICENSE) - ---- - -![Star History Chart](https://api.star-history.com/svg?repos=opencut-app/opencut&type=Date) - diff --git a/apps/desktop/.gitignore b/apps/desktop/.gitignore new file mode 100644 index 00000000..090e2f63 --- /dev/null +++ b/apps/desktop/.gitignore @@ -0,0 +1,4 @@ +dist/ +web/ +release/ +node_modules/ diff --git a/apps/desktop/Cargo.toml b/apps/desktop/Cargo.toml deleted file mode 100644 index e09443ab..00000000 --- a/apps/desktop/Cargo.toml +++ /dev/null @@ -1,11 +0,0 @@ -[package] -name = "opencut-desktop" -version = "0.1.0" -edition = "2021" - -[[bin]] -name = "opencut" -path = "src/main.rs" - -[dependencies] -gpui = "0.2.2" diff --git a/apps/desktop/README.md b/apps/desktop/README.md deleted file mode 100644 index 9cff04e1..00000000 --- a/apps/desktop/README.md +++ /dev/null @@ -1,47 +0,0 @@ -# Desktop - -The native desktop app, built with [GPUI](https://gpui.rs). - -## Getting started - -**1. Install Rust:** - -```bash -# Linux / macOS / WSL -./script/setup-rust -``` - -```powershell -# Windows -powershell -ExecutionPolicy Bypass -File .\script\setup-rust.ps1 -``` - -Both scripts skip installation if Rust is already present. On Linux/macOS/WSL only: after a fresh install, reload your shell with `source "$HOME/.cargo/env"` - -**2. Install native dependencies:** - -```bash -# Linux / macOS / WSL -./apps/desktop/script/setup -``` - -```powershell -# Windows -powershell -ExecutionPolicy Bypass -File .\apps\desktop\script\setup.ps1 -``` - -**3. Run:** - -```bash -cargo run -p opencut-desktop -``` - -## Platform notes - -**Linux:** supports apt (Debian/Ubuntu/Mint), dnf (Fedora/RHEL), and pacman (Arch). - -**macOS:** installs Xcode Command Line Tools if missing. - -**Windows:** the setup script checks for Visual Studio Build Tools. If missing, it prints the install link. - -**WSL:** runs the same scripts as Linux. Window rendering works via WSLg on Windows 11 and Windows 10 22H2+. If you're on an older build, test on the host instead. diff --git a/apps/desktop/build/icon.png b/apps/desktop/build/icon.png new file mode 100644 index 00000000..f534f429 Binary files /dev/null and b/apps/desktop/build/icon.png differ diff --git a/apps/desktop/electron-builder.yml b/apps/desktop/electron-builder.yml new file mode 100644 index 00000000..b47cf1d7 --- /dev/null +++ b/apps/desktop/electron-builder.yml @@ -0,0 +1,28 @@ +appId: rs.cashmere.opencut +productName: OpenCut +directories: + output: release +files: + - dist/** + - package.json +asarUnpack: + - "**/ffmpeg-static/**" +extraResources: + - from: web + to: web +linux: + executableName: opencut + icon: build/icon.png + syncDesktopName: true + category: AudioVideo;Video + target: + - AppImage + - deb + - rpm + artifactName: OpenCut-${version}-linux-${arch}.${ext} +deb: + maintainer: cashmere + description: Offline-first video editor +rpm: + maintainer: cashmere + description: Offline-first video editor diff --git a/apps/desktop/package.json b/apps/desktop/package.json new file mode 100644 index 00000000..99934d7b --- /dev/null +++ b/apps/desktop/package.json @@ -0,0 +1,25 @@ +{ + "name": "@opencut/desktop", + "version": "0.1.0", + "private": true, + "main": "dist/main.js", + "scripts": { + "build": "tsc -p tsconfig.json", + "dev": "bun run build && electron .", + "pack": "bun run build && electron-builder --dir", + "dist": "bun run build && electron-builder --linux AppImage deb rpm", + "copy:web": "node scripts/copy-web.mjs" + }, + "dependencies": { + "ffmpeg-static": "^5.3.0" + }, + "devDependencies": { + "@types/node": "^26.1.2", + "electron": "^43.2.0", + "electron-builder": "^26.15.3", + "typescript": "^7.0.2" + }, + "author": "cashmere", + "homepage": "https://github.com/cashmeredev/opencut", + "desktopName": "OpenCut" +} diff --git a/apps/desktop/script/setup b/apps/desktop/script/setup deleted file mode 100644 index 3152a080..00000000 --- a/apps/desktop/script/setup +++ /dev/null @@ -1,110 +0,0 @@ -#!/usr/bin/env bash - -# Installs the native system libraries GPUI needs to compile on Linux/macOS. -# Run script/setup-rust first if you don't have Rust installed yet. - -set -euo pipefail - -if [ "$(id -u)" -eq 0 ]; then - maysudo='' -else - maysudo="$(command -v sudo || command -v doas || true)" -fi - -# Ubuntu, Debian, Mint, Pop!_OS, Kali, Raspbian, etc. -apt=$(command -v apt-get || true) -if [[ -n $apt ]]; then - deps=( - curl - build-essential - pkg-config - gcc - g++ - make - libasound2-dev - libfontconfig-dev - libglib2.0-dev - libssl-dev - libva-dev - libvulkan1 - libwayland-dev - libx11-xcb-dev - libxkbcommon-x11-dev - libzstd-dev - libsqlite3-dev - ) - - $maysudo "$apt" update - $maysudo "$apt" install -y "${deps[@]}" - echo "Desktop native dependencies installed." - exit 0 -fi - -# Fedora, RHEL, CentOS, Alma, etc. -dnf=$(command -v dnf || true) -if [[ -n $dnf ]]; then - deps=( - gcc - gcc-c++ - make - pkg-config - alsa-lib-devel - fontconfig-devel - glib2-devel - openssl-devel - libva-devel - vulkan-loader - wayland-devel - libxcb-devel - libxkbcommon-x11-devel - libzstd-devel - sqlite-devel - curl - ) - - $maysudo "$dnf" install -y "${deps[@]}" - echo "Desktop native dependencies installed." - exit 0 -fi - -# Arch, Manjaro, etc. -pacman=$(command -v pacman || true) -if [[ -n $pacman ]]; then - deps=( - gcc - make - pkgconf - alsa-lib - fontconfig - glib2 - openssl - libva - vulkan-loader - wayland - libxcb - libxkbcommon-x11 - zstd - sqlite - curl - ) - - $maysudo "$pacman" -Syu --needed --noconfirm "${deps[@]}" - echo "Desktop native dependencies installed." - exit 0 -fi - -# macOS -if [[ "$(uname)" == "Darwin" ]]; then - if ! xcode-select -p &> /dev/null; then - echo "Installing Xcode Command Line Tools..." - xcode-select --install - echo "Re-run this script after the Xcode install completes." - exit 0 - fi - - echo "Desktop native dependencies installed." - exit 0 -fi - -echo "Unsupported platform. See apps/desktop/README.md for manual setup instructions." -exit 1 diff --git a/apps/desktop/script/setup.ps1 b/apps/desktop/script/setup.ps1 deleted file mode 100644 index 63f7fd1a..00000000 --- a/apps/desktop/script/setup.ps1 +++ /dev/null @@ -1,27 +0,0 @@ -# Installs the native build tools GPUI needs to compile on Windows. -# Run script/setup-rust.ps1 first if you don't have Rust installed yet. - -$ErrorActionPreference = "Stop" - -function Check-VSBuildTools { - $vswhere = "C:\Program Files (x86)\Microsoft Visual Studio\Installer\vswhere.exe" - - if (-not (Test-Path $vswhere)) { - return $false - } - - $result = & $vswhere -latest -requires Microsoft.VisualStudio.Workload.NativeDesktop -property installationPath - return $result -and $result.Trim().Length -gt 0 -} - -if (-not (Check-VSBuildTools)) { - Write-Output "" - Write-Output "Visual Studio Build Tools with 'Desktop development with C++' not found." - Write-Output "" - Write-Output "Install it from: https://visualstudio.microsoft.com/visual-cpp-build-tools/" - Write-Output "Check the 'Desktop development with C++' workload during setup, then re-run this script." - Write-Output "" - exit 1 -} - -Write-Output "Desktop native dependencies ready." diff --git a/apps/desktop/scripts/copy-web.mjs b/apps/desktop/scripts/copy-web.mjs new file mode 100644 index 00000000..f5f6f4f6 --- /dev/null +++ b/apps/desktop/scripts/copy-web.mjs @@ -0,0 +1,18 @@ +import { cpSync, existsSync, rmSync } from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +const desktopDir = path.dirname(path.dirname(fileURLToPath(import.meta.url))); +const source = path.join(desktopDir, "..", "web", "out"); +const target = path.join(desktopDir, "web"); + +if (!existsSync(source)) { + process.stderr.write( + `Web bundle not found at ${source}. Run \`bun build:web\` first.\n`, + ); + process.exit(1); +} + +rmSync(target, { recursive: true, force: true }); +cpSync(source, target, { recursive: true }); +process.stdout.write(`Copied ${source} -> ${target}\n`); diff --git a/apps/desktop/src/export.ts b/apps/desktop/src/export.ts new file mode 100644 index 00000000..4cbfe613 --- /dev/null +++ b/apps/desktop/src/export.ts @@ -0,0 +1,293 @@ +import { app, BrowserWindow, dialog, ipcMain, shell } from "electron"; +import { type ChildProcessByStdio, spawn } from "node:child_process"; +import type { Readable, Writable } from "node:stream"; +import { randomUUID } from "node:crypto"; +import { unlink, writeFile } from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import ffmpegStaticPath from "ffmpeg-static"; + +export type DesktopExportBegin = { + width: number; + height: number; + fpsNumerator: number; + fpsDenominator: number; + format: "mp4" | "webm"; + quality: "low" | "medium" | "high" | "very_high"; + hasAudio: boolean; + sampleRate: number; + channels: number; + defaultFileName: string; +}; + +type ExportQuality = DesktopExportBegin["quality"]; + +type FfmpegProcess = ChildProcessByStdio; + +type ExportSession = { + id: string; + filePath: string; + opts: DesktopExportBegin; + audioPath: string | null; + process: FfmpegProcess | null; + exitCode: Promise | null; + stderrTail: string; +}; + +const MP4_CRF: Record = { + low: 28, + medium: 23, + high: 19, + very_high: 15, +}; + +const WEBM_CRF: Record = { + low: 40, + medium: 32, + high: 24, + very_high: 16, +}; + +const STDERR_TAIL_LIMIT = 8192; + +const sessions = new Map(); + +function resolveFfmpegPath(): string { + if (!ffmpegStaticPath) { + throw new Error("ffmpeg binary is not available for this platform"); + } + if (app.isPackaged) { + return ffmpegStaticPath.replace("app.asar", "app.asar.unpacked"); + } + return ffmpegStaticPath; +} + +function buildFfmpegArgs(session: ExportSession): string[] { + const { opts, filePath, audioPath } = session; + + const args = [ + "-y", + "-f", + "rawvideo", + "-pix_fmt", + "rgba", + "-s", + `${opts.width}x${opts.height}`, + "-r", + `${opts.fpsNumerator}/${opts.fpsDenominator}`, + "-i", + "pipe:0", + ]; + + if (audioPath) { + args.push( + "-f", + "f32le", + "-ar", + String(opts.sampleRate), + "-ac", + String(opts.channels), + "-i", + audioPath, + ); + } + + if (opts.format === "mp4") { + args.push( + "-c:v", + "libx264", + "-preset", + "medium", + "-crf", + String(MP4_CRF[opts.quality]), + "-pix_fmt", + "yuv420p", + "-color_primaries", + "bt709", + "-color_trc", + "bt709", + "-colorspace", + "bt709", + ); + if (audioPath) { + args.push("-c:a", "aac", "-b:a", "192k"); + } + args.push("-movflags", "+faststart"); + } else { + args.push( + "-c:v", + "libvpx-vp9", + "-deadline", + "good", + "-cpu-used", + "2", + "-row-mt", + "1", + "-crf", + String(WEBM_CRF[opts.quality]), + "-b:v", + "0", + "-pix_fmt", + "yuv420p", + ); + if (audioPath) { + args.push("-c:a", "libopus", "-b:a", "192k"); + } + } + + if (audioPath) { + args.push("-shortest"); + } + + args.push(filePath); + return args; +} + +function getSession(id: string): ExportSession { + const session = sessions.get(id); + if (!session) { + throw new Error(`Unknown export session: ${id}`); + } + return session; +} + +function toBuffer(data: ArrayBuffer | Uint8Array): Buffer { + if (data instanceof Uint8Array) { + return Buffer.from(data.buffer, data.byteOffset, data.byteLength); + } + return Buffer.from(data); +} + +function spawnFfmpeg(session: ExportSession): FfmpegProcess { + const proc = spawn(resolveFfmpegPath(), buildFfmpegArgs(session), { + stdio: ["pipe", "ignore", "pipe"], + }); + + proc.stderr.on("data", (chunk: Buffer) => { + session.stderrTail = (session.stderrTail + chunk.toString()).slice( + -STDERR_TAIL_LIMIT, + ); + }); + + session.exitCode = new Promise((resolve, reject) => { + proc.on("error", reject); + proc.on("close", (code) => resolve(code ?? 1)); + }); + session.exitCode.catch(() => {}); + session.process = proc; + return proc; +} + +async function teardownSession(session: ExportSession): Promise { + if (session.process) { + session.process.kill("SIGKILL"); + await session.exitCode?.catch(() => {}); + } + if (session.audioPath) { + await unlink(session.audioPath).catch(() => {}); + } + await unlink(session.filePath).catch(() => {}); +} + +export function registerExportHandlers(): void { + ipcMain.handle("export:begin", async (event, opts: DesktopExportBegin) => { + const win = BrowserWindow.fromWebContents(event.sender); + const filters = + opts.format === "mp4" + ? [{ name: "MP4 Video", extensions: ["mp4"] }] + : [{ name: "WebM Video", extensions: ["webm"] }]; + const saveOptions = { defaultPath: opts.defaultFileName, filters }; + const result = win + ? await dialog.showSaveDialog(win, saveOptions) + : await dialog.showSaveDialog(saveOptions); + + if (result.canceled || !result.filePath) { + return null; + } + + const id = randomUUID(); + sessions.set(id, { + id, + filePath: result.filePath, + opts, + audioPath: null, + process: null, + exitCode: null, + stderrTail: "", + }); + return { id, filePath: result.filePath }; + }); + + ipcMain.handle( + "export:writeAudio", + async (_event, id: string, pcm: ArrayBuffer | Uint8Array) => { + const session = getSession(id); + const audioPath = path.join(os.tmpdir(), `opencut-audio-${id}.f32`); + await writeFile(audioPath, toBuffer(pcm)); + session.audioPath = audioPath; + }, + ); + + ipcMain.handle( + "export:writeFrame", + async (_event, id: string, rgba: ArrayBuffer | Uint8Array) => { + const session = getSession(id); + const proc = session.process ?? spawnFfmpeg(session); + const buffer = toBuffer(rgba); + + await new Promise((resolve, reject) => { + const onError = (error: Error) => reject(error); + proc.stdin.once("error", onError); + if (proc.stdin.write(buffer)) { + proc.stdin.removeListener("error", onError); + resolve(); + return; + } + proc.stdin.once("drain", () => { + proc.stdin.removeListener("error", onError); + resolve(); + }); + }); + }, + ); + + ipcMain.handle("export:finish", async (_event, id: string) => { + const session = getSession(id); + sessions.delete(id); + + try { + if (session.process && session.exitCode) { + session.process.stdin.end(); + const code = await session.exitCode; + if (code !== 0) { + const tail = session.stderrTail.trim(); + throw new Error( + `ffmpeg exited with code ${code}${tail ? `: ${tail}` : ""}`, + ); + } + } + } finally { + if (session.audioPath) { + await unlink(session.audioPath).catch(() => {}); + } + } + }); + + ipcMain.handle("export:cancel", async (_event, id: string) => { + const session = sessions.get(id); + if (!session) return; + sessions.delete(id); + await teardownSession(session); + }); + + ipcMain.handle("reveal-file", (_event, filePath: string) => { + shell.showItemInFolder(filePath); + }); +} + +export function cancelAllExports(): void { + for (const session of sessions.values()) { + session.process?.kill("SIGKILL"); + } + sessions.clear(); +} diff --git a/apps/desktop/src/main.rs b/apps/desktop/src/main.rs deleted file mode 100644 index 0ba70959..00000000 --- a/apps/desktop/src/main.rs +++ /dev/null @@ -1,41 +0,0 @@ -use gpui::{ - div, prelude::*, px, rgb, size, App, Application, Bounds, Context, SharedString, Window, - WindowBounds, WindowOptions, -}; - -struct AppWindow { - title: SharedString, -} - -impl Render for AppWindow { - fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { - div() - .size_full() - .bg(rgb(0x0f0f0f)) - .flex() - .justify_center() - .items_center() - .text_xl() - .text_color(rgb(0xffffff)) - .child(self.title.clone()) - } -} - -fn main() { - Application::new().run(|cx: &mut App| { - let bounds = Bounds::centered(None, size(px(1280.), px(720.)), cx); - cx.open_window( - WindowOptions { - window_bounds: Some(WindowBounds::Windowed(bounds)), - ..Default::default() - }, - |_, cx| { - cx.new(|_| AppWindow { - title: "OpenCut".into(), - }) - }, - ) - .unwrap(); - cx.activate(true); - }); -} diff --git a/apps/desktop/src/main.ts b/apps/desktop/src/main.ts new file mode 100644 index 00000000..fccd7c1d --- /dev/null +++ b/apps/desktop/src/main.ts @@ -0,0 +1,131 @@ +import { app, BrowserWindow, protocol } from "electron"; +import { stat } from "node:fs/promises"; +import { createReadStream } from "node:fs"; +import path from "node:path"; +import { Readable } from "node:stream"; +import { cancelAllExports, registerExportHandlers } from "./export.js"; + +const contentTypes: Record = { + ".html": "text/html; charset=utf-8", + ".js": "text/javascript; charset=utf-8", + ".css": "text/css; charset=utf-8", + ".json": "application/json; charset=utf-8", + ".wasm": "application/wasm", + ".png": "image/png", + ".jpg": "image/jpeg", + ".svg": "image/svg+xml", + ".ico": "image/x-icon", + ".woff2": "font/woff2", + ".mp4": "video/mp4", + ".webm": "video/webm", + ".txt": "text/plain; charset=utf-8", + ".xml": "application/xml; charset=utf-8", + ".map": "application/json; charset=utf-8", +}; + +protocol.registerSchemesAsPrivileged([ + { + scheme: "opencut", + privileges: { + standard: true, + secure: true, + supportFetchAPI: true, + stream: true, + bypassCSP: false, + }, + }, +]); + +function registerWebProtocol(): void { + const webDir = app.isPackaged + ? path.join(process.resourcesPath, "web") + : path.join(__dirname, "..", "web"); + + protocol.handle("opencut", async (request) => { + const url = new URL(request.url); + const pathname = decodeURIComponent(url.pathname); + const resolved = path.normalize(path.join(webDir, pathname)); + + if (resolved !== webDir && !resolved.startsWith(`${webDir}${path.sep}`)) { + return new Response("Forbidden", { status: 403 }); + } + + let filePath = resolved; + let info = await stat(filePath).catch(() => null); + + if (info?.isDirectory() || (!info && path.extname(filePath) === "")) { + filePath = path.join(resolved, "index.html"); + info = await stat(filePath).catch(() => null); + } + + if (!info?.isFile()) { + return new Response("Not found", { status: 404 }); + } + + const contentType = contentTypes[path.extname(filePath).toLowerCase()]; + if (!contentType) { + return new Response("Not found", { status: 404 }); + } + + const stream = Readable.toWeb(createReadStream(filePath)); + return new Response(stream as ReadableStream, { + headers: { "Content-Type": contentType }, + }); + }); +} + +function createWindow(): void { + const win = new BrowserWindow({ + width: 1440, + height: 900, + minWidth: 960, + minHeight: 600, + backgroundColor: "#0a0a0a", + autoHideMenuBar: true, + webPreferences: { + preload: path.join(__dirname, "preload.js"), + contextIsolation: true, + nodeIntegration: false, + sandbox: false, + }, + }); + + const devServerUrl = process.env.OPENCUT_DEV_SERVER_URL; + if (devServerUrl) { + win.loadURL(devServerUrl); + return; + } + + win.loadURL("opencut://app/"); +} + +const gotSingleInstanceLock = app.requestSingleInstanceLock(); + +if (!gotSingleInstanceLock) { + app.quit(); +} else { + app.on("second-instance", () => { + const [win] = BrowserWindow.getAllWindows(); + if (!win) return; + if (win.isMinimized()) win.restore(); + win.focus(); + }); + + app.whenReady().then(() => { + registerWebProtocol(); + registerExportHandlers(); + createWindow(); + + app.on("activate", () => { + if (BrowserWindow.getAllWindows().length === 0) createWindow(); + }); + }); +} + +app.on("window-all-closed", () => { + app.quit(); +}); + +app.on("will-quit", () => { + cancelAllExports(); +}); diff --git a/apps/desktop/src/preload.ts b/apps/desktop/src/preload.ts new file mode 100644 index 00000000..ad3f6cf9 --- /dev/null +++ b/apps/desktop/src/preload.ts @@ -0,0 +1,22 @@ +import { contextBridge, ipcRenderer } from "electron"; +import type { DesktopExportBegin } from "./export.js"; + +const api = { + isDesktop: true as const, + exportBegin: ( + opts: DesktopExportBegin, + ): Promise<{ id: string; filePath: string } | null> => + ipcRenderer.invoke("export:begin", opts), + exportWriteAudio: (id: string, pcm: ArrayBuffer): Promise => + ipcRenderer.invoke("export:writeAudio", id, pcm), + exportWriteFrame: (id: string, rgba: ArrayBuffer): Promise => + ipcRenderer.invoke("export:writeFrame", id, rgba), + exportFinish: (id: string): Promise => + ipcRenderer.invoke("export:finish", id), + exportCancel: (id: string): Promise => + ipcRenderer.invoke("export:cancel", id), + revealFile: (path: string): Promise => + ipcRenderer.invoke("reveal-file", path), +}; + +contextBridge.exposeInMainWorld("opencutDesktop", api); diff --git a/apps/desktop/tsconfig.json b/apps/desktop/tsconfig.json new file mode 100644 index 00000000..1948e8df --- /dev/null +++ b/apps/desktop/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "lib": ["ES2022"], + "outDir": "dist", + "rootDir": "src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "sourceMap": false, + "types": ["node"] + }, + "include": ["src"] +} diff --git a/apps/web/.env.example b/apps/web/.env.example deleted file mode 100644 index b15779a5..00000000 --- a/apps/web/.env.example +++ /dev/null @@ -1,21 +0,0 @@ -# Environment variables example -# Copy this file to .env.local and update the values as needed - -# Node -NODE_ENV=development - -# Public -NEXT_PUBLIC_SITE_URL=http://localhost:3000 -NEXT_PUBLIC_MARBLE_API_URL=https://api.marblecms.com - -# Server -DATABASE_URL="postgresql://opencut:opencut@localhost:5432/opencut" -BETTER_AUTH_SECRET=your_better_auth_secret - -UPSTASH_REDIS_REST_URL=http://localhost:8079 -UPSTASH_REDIS_REST_TOKEN=example_token - -MARBLE_WORKSPACE_KEY=your_workspace_key_here - -FREESOUND_CLIENT_ID=your_client_id_here -FREESOUND_API_KEY=your_api_key_here diff --git a/apps/web/.gitignore b/apps/web/.gitignore index ddb836b6..2a8d87bb 100644 --- a/apps/web/.gitignore +++ b/apps/web/.gitignore @@ -12,3 +12,6 @@ next-env.d.ts *.tsbuildinfo + +# Static export +out/ diff --git a/apps/web/Dockerfile b/apps/web/Dockerfile index a08d9b68..27a9db70 100644 --- a/apps/web/Dockerfile +++ b/apps/web/Dockerfile @@ -1,62 +1,21 @@ -FROM oven/bun:alpine AS base - -FROM base AS builder - -WORKDIR /app - -ARG FREESOUND_CLIENT_ID -ARG FREESOUND_API_KEY -ARG NEXT_PUBLIC_MARBLE_API_URL=https://api.marblecms.com -ARG MARBLE_WORKSPACE_KEY=build-placeholder - -COPY package.json package.json -COPY bun.lock bun.lock -COPY turbo.json turbo.json - -COPY apps/web/package.json apps/web/package.json -RUN bun install - -COPY apps/web/ apps/web/ - -ENV NODE_ENV=production -ENV NEXT_TELEMETRY_DISABLED=1 - -# Build-time env stubs to pass zod validation -ENV DATABASE_URL="postgresql://opencut:opencut@localhost:5432/opencut" -ENV BETTER_AUTH_SECRET="build-time-secret" -ENV UPSTASH_REDIS_REST_URL="http://localhost:8079" -ENV UPSTASH_REDIS_REST_TOKEN="example_token" -ENV NEXT_PUBLIC_SITE_URL="http://localhost:3000" -ENV NEXT_PUBLIC_MARBLE_API_URL=$NEXT_PUBLIC_MARBLE_API_URL -ENV MARBLE_WORKSPACE_KEY=$MARBLE_WORKSPACE_KEY - -ENV FREESOUND_CLIENT_ID=$FREESOUND_CLIENT_ID -ENV FREESOUND_API_KEY=$FREESOUND_API_KEY - -WORKDIR /app/apps/web -RUN bun run build - -# Production image -FROM base AS runner -WORKDIR /app - -ENV NODE_ENV=production -ENV NEXT_TELEMETRY_DISABLED=1 - -RUN addgroup --system --gid 1001 nodejs -RUN adduser --system --uid 1001 nextjs - -COPY --from=builder --chown=nextjs:nodejs /app/apps/web/public ./apps/web/public -COPY --from=builder --chown=nextjs:nodejs /app/apps/web/.next/standalone ./ -COPY --from=builder --chown=nextjs:nodejs /app/apps/web/.next/static ./apps/web/.next/static - -RUN chown nextjs:nodejs apps - -USER nextjs - -EXPOSE 3000 - -ENV PORT=3000 -ENV HOSTNAME="0.0.0.0" - -CMD ["bun", "apps/web/server.js"] +FROM oven/bun:1 AS builder + +WORKDIR /app + +COPY . . + +RUN bun install --frozen-lockfile + +ENV NODE_ENV=production +ENV NEXT_TELEMETRY_DISABLED=1 + +WORKDIR /app/apps/web +RUN bun run build + +FROM nginx:alpine AS runner + +COPY --from=builder /app/apps/web/out /usr/share/nginx/html + +EXPOSE 80 + +CMD ["nginx", "-g", "daemon off;"] diff --git a/apps/web/content-collections.ts b/apps/web/content-collections.ts index 0fdd7234..56be4030 100644 --- a/apps/web/content-collections.ts +++ b/apps/web/content-collections.ts @@ -3,7 +3,7 @@ import { z } from "zod"; const changelog = defineCollection({ name: "changelog", - directory: "src/lib/changelog/entries", + directory: "src/changelog/entries", include: "*.md", schema: z.object({ content: z.string(), diff --git a/apps/web/drizzle.config.ts b/apps/web/drizzle.config.ts deleted file mode 100644 index f3c4b357..00000000 --- a/apps/web/drizzle.config.ts +++ /dev/null @@ -1,26 +0,0 @@ -import type { Config } from "drizzle-kit"; -import * as dotenv from "dotenv"; - -if (process.env.NODE_ENV === "production") { - dotenv.config({ path: ".env.production" }); -} else { - dotenv.config({ path: ".env.local" }); -} - -const databaseUrl = process.env.DATABASE_URL; -if (!databaseUrl) { - throw new Error("DATABASE_URL is not set"); -} - -export default { - schema: "./src/lib/db/schema.ts", - dialect: "postgresql", - migrations: { - table: "drizzle_migrations", - }, - dbCredentials: { - url: databaseUrl, - }, - out: "./migrations", - strict: process.env.NODE_ENV === "production", -} satisfies Config; diff --git a/apps/web/migrations/0000_brainy_saracen.sql b/apps/web/migrations/0000_brainy_saracen.sql deleted file mode 100644 index 3179d6c5..00000000 --- a/apps/web/migrations/0000_brainy_saracen.sql +++ /dev/null @@ -1,62 +0,0 @@ -CREATE TABLE "accounts" ( - "id" text PRIMARY KEY NOT NULL, - "account_id" text NOT NULL, - "provider_id" text NOT NULL, - "user_id" text NOT NULL, - "access_token" text, - "refresh_token" text, - "id_token" text, - "access_token_expires_at" timestamp, - "refresh_token_expires_at" timestamp, - "scope" text, - "password" text, - "created_at" timestamp NOT NULL, - "updated_at" timestamp NOT NULL -); ---> statement-breakpoint -ALTER TABLE "accounts" ENABLE ROW LEVEL SECURITY;--> statement-breakpoint -CREATE TABLE "sessions" ( - "id" text PRIMARY KEY NOT NULL, - "expires_at" timestamp NOT NULL, - "token" text NOT NULL, - "created_at" timestamp NOT NULL, - "updated_at" timestamp NOT NULL, - "ip_address" text, - "user_agent" text, - "user_id" text NOT NULL, - CONSTRAINT "sessions_token_unique" UNIQUE("token") -); ---> statement-breakpoint -ALTER TABLE "sessions" ENABLE ROW LEVEL SECURITY;--> statement-breakpoint -CREATE TABLE "users" ( - "id" text PRIMARY KEY NOT NULL, - "name" text NOT NULL, - "email" text NOT NULL, - "email_verified" boolean NOT NULL, - "image" text, - "created_at" timestamp NOT NULL, - "updated_at" timestamp NOT NULL, - CONSTRAINT "users_email_unique" UNIQUE("email") -); ---> statement-breakpoint -ALTER TABLE "users" ENABLE ROW LEVEL SECURITY;--> statement-breakpoint -CREATE TABLE "verifications" ( - "id" text PRIMARY KEY NOT NULL, - "identifier" text NOT NULL, - "value" text NOT NULL, - "expires_at" timestamp NOT NULL, - "created_at" timestamp, - "updated_at" timestamp -); ---> statement-breakpoint -ALTER TABLE "verifications" ENABLE ROW LEVEL SECURITY;--> statement-breakpoint -CREATE TABLE "waitlist" ( - "id" text PRIMARY KEY NOT NULL, - "email" text NOT NULL, - "created_at" timestamp NOT NULL, - CONSTRAINT "waitlist_email_unique" UNIQUE("email") -); ---> statement-breakpoint -ALTER TABLE "waitlist" ENABLE ROW LEVEL SECURITY;--> statement-breakpoint -ALTER TABLE "accounts" ADD CONSTRAINT "accounts_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint -ALTER TABLE "sessions" ADD CONSTRAINT "sessions_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action; \ No newline at end of file diff --git a/apps/web/migrations/meta/0000_snapshot.json b/apps/web/migrations/meta/0000_snapshot.json deleted file mode 100644 index 42bae1c6..00000000 --- a/apps/web/migrations/meta/0000_snapshot.json +++ /dev/null @@ -1,344 +0,0 @@ -{ - "id": "33a6742f-89da-4ac5-958f-421aa1cf9bd6", - "prevId": "00000000-0000-0000-0000-000000000000", - "version": "7", - "dialect": "postgresql", - "tables": { - "public.accounts": { - "name": "accounts", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "account_id": { - "name": "account_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "provider_id": { - "name": "provider_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "user_id": { - "name": "user_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "access_token": { - "name": "access_token", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "refresh_token": { - "name": "refresh_token", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "id_token": { - "name": "id_token", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "access_token_expires_at": { - "name": "access_token_expires_at", - "type": "timestamp", - "primaryKey": false, - "notNull": false - }, - "refresh_token_expires_at": { - "name": "refresh_token_expires_at", - "type": "timestamp", - "primaryKey": false, - "notNull": false - }, - "scope": { - "name": "scope", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "password": { - "name": "password", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "created_at": { - "name": "created_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true - } - }, - "indexes": {}, - "foreignKeys": { - "accounts_user_id_users_id_fk": { - "name": "accounts_user_id_users_id_fk", - "tableFrom": "accounts", - "tableTo": "users", - "columnsFrom": ["user_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": true - }, - "public.sessions": { - "name": "sessions", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "expires_at": { - "name": "expires_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true - }, - "token": { - "name": "token", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "created_at": { - "name": "created_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true - }, - "ip_address": { - "name": "ip_address", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "user_agent": { - "name": "user_agent", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "user_id": { - "name": "user_id", - "type": "text", - "primaryKey": false, - "notNull": true - } - }, - "indexes": {}, - "foreignKeys": { - "sessions_user_id_users_id_fk": { - "name": "sessions_user_id_users_id_fk", - "tableFrom": "sessions", - "tableTo": "users", - "columnsFrom": ["user_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "sessions_token_unique": { - "name": "sessions_token_unique", - "nullsNotDistinct": false, - "columns": ["token"] - } - }, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": true - }, - "public.users": { - "name": "users", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "email": { - "name": "email", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "email_verified": { - "name": "email_verified", - "type": "boolean", - "primaryKey": false, - "notNull": true - }, - "image": { - "name": "image", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "created_at": { - "name": "created_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "users_email_unique": { - "name": "users_email_unique", - "nullsNotDistinct": false, - "columns": ["email"] - } - }, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": true - }, - "public.verifications": { - "name": "verifications", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "identifier": { - "name": "identifier", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "value": { - "name": "value", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "expires_at": { - "name": "expires_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true - }, - "created_at": { - "name": "created_at", - "type": "timestamp", - "primaryKey": false, - "notNull": false - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp", - "primaryKey": false, - "notNull": false - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": true - }, - "public.waitlist": { - "name": "waitlist", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "email": { - "name": "email", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "created_at": { - "name": "created_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "waitlist_email_unique": { - "name": "waitlist_email_unique", - "nullsNotDistinct": false, - "columns": ["email"] - } - }, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": true - } - }, - "enums": {}, - "schemas": {}, - "sequences": {}, - "roles": {}, - "policies": {}, - "views": {}, - "_meta": { - "columns": {}, - "schemas": {}, - "tables": {} - } -} diff --git a/apps/web/migrations/meta/_journal.json b/apps/web/migrations/meta/_journal.json deleted file mode 100644 index a91398ed..00000000 --- a/apps/web/migrations/meta/_journal.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "version": "7", - "dialect": "postgresql", - "entries": [ - { - "idx": 0, - "version": "7", - "when": 1750753385927, - "tag": "0000_brainy_saracen", - "breakpoints": true - } - ] -} diff --git a/apps/web/next.config.ts b/apps/web/next.config.ts index 66babe11..644c7079 100644 --- a/apps/web/next.config.ts +++ b/apps/web/next.config.ts @@ -1,5 +1,4 @@ import type { NextConfig } from "next"; -import { withBotId } from "botid/next/config"; import { withContentCollections } from "@content-collections/next"; const nextConfig: NextConfig = { @@ -9,47 +8,11 @@ const nextConfig: NextConfig = { devIndicators: false, reactStrictMode: true, productionBrowserSourceMaps: true, - output: "standalone", + output: "export", + trailingSlash: true, images: { - remotePatterns: [ - { - protocol: "https", - hostname: "plus.unsplash.com", - }, - { - protocol: "https", - hostname: "images.unsplash.com", - }, - { - protocol: "https", - hostname: "images.marblecms.com", - }, - { - protocol: "https", - hostname: "lh3.googleusercontent.com", - }, - { - protocol: "https", - hostname: "avatars.githubusercontent.com", - }, - { - protocol: "https", - hostname: "api.iconify.design", - }, - { - protocol: "https", - hostname: "api.simplesvg.com", - }, - { - protocol: "https", - hostname: "api.unisvg.com", - }, - { - protocol: "https", - hostname: "cdn.brandfetch.io", - }, - ], + unoptimized: true, }, }; -export default withContentCollections(withBotId(nextConfig)); +export default withContentCollections(nextConfig); diff --git a/apps/web/open-next.config.ts b/apps/web/open-next.config.ts deleted file mode 100644 index 36144eb1..00000000 --- a/apps/web/open-next.config.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { defineCloudflareConfig } from "@opennextjs/cloudflare"; - -export default defineCloudflareConfig(); \ No newline at end of file diff --git a/apps/web/package.json b/apps/web/package.json index 816dfe01..d8b4f333 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -7,22 +7,15 @@ "dev": "next dev --turbopack", "build": "next build", "start": "next start", - "preview": "opennextjs-cloudflare build && opennextjs-cloudflare preview", - "deploy": "opennextjs-cloudflare build && opennextjs-cloudflare deploy", "lint": "eslint src --ext .ts,.tsx", "lint:fix": "eslint src --ext .ts,.tsx --fix", - "format": "prettier src --write", - "db:generate": "drizzle-kit generate", - "db:migrate": "drizzle-kit migrate", - "db:push:local": "cross-env NODE_ENV=development drizzle-kit push", - "db:push:prod": "cross-env NODE_ENV=production drizzle-kit push" + "format": "prettier src --write" }, "dependencies": { "@hello-pangea/dnd": "^18.0.1", "@hugeicons/core-free-icons": "^3.3.0", "@hugeicons/react": "^1.1.6", "@huggingface/transformers": "^3.8.1", - "@opennextjs/cloudflare": "^1.18.0", "@radix-ui/react-accordion": "^1.2.12", "@radix-ui/react-checkbox": "^1.3.3", "@radix-ui/react-dialog": "^1.1.15", @@ -33,18 +26,12 @@ "@radix-ui/react-slot": "^1.2.4", "@radix-ui/react-tooltip": "^1.2.8", "@types/culori": "^4.0.1", - "@upstash/ratelimit": "^2.0.6", - "@upstash/redis": "^1.35.4", - "better-auth": "^1.2.7", - "botid": "^1.5.11", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.0.0", "culori": "^4.0.2", - "drizzle-orm": "^0.44.2", "embla-carousel-react": "^8.5.1", "eventemitter3": "^5.0.1", - "feed": "^5.1.0", "fflate": "^0.8.3", "input-otp": "^1.4.1", "lucide-react": "^0.562.0", @@ -54,8 +41,6 @@ "next": "16.1.3", "next-themes": "^0.4.4", "opencut-wasm": "^0.2.10", - "pg": "^8.16.2", - "postgres": "^3.4.5", "radix-ui": "^1.4.3", "react": "^19.0.0", "react-day-picker": "^8.10.1", @@ -65,11 +50,7 @@ "react-markdown": "^10.1.0", "react-resizable-panels": "^2.1.7", "react-window": "^2.2.7", - "rehype-autolink-headings": "^7.1.0", "rehype-parse": "^9.0.1", - "rehype-sanitize": "^6.0.0", - "rehype-slug": "^6.0.0", - "rehype-stringify": "^10.0.1", "sonner": "^1.7.1", "soundtouchjs": "^0.3.0", "tailwind-merge": "^3.5.0", @@ -88,16 +69,11 @@ "@tailwindcss/typography": "^0.5.19", "@types/bun": "latest", "@types/node": "^24.2.1", - "@types/pg": "^8.15.4", "@types/react": "^19", "@types/react-dom": "^19", - "cross-env": "^7.0.3", - "dotenv": "^16.5.0", - "drizzle-kit": "^0.31.4", "postcss": "^8", "sharp": "^0.34.5", "tailwindcss": "^4.2.1", - "typescript": "^5.8.3", - "wrangler": "^4.77.0" + "typescript": "^5.8.3" } } diff --git a/apps/web/src/app/api/auth/[...all]/route.ts b/apps/web/src/app/api/auth/[...all]/route.ts deleted file mode 100644 index edf0d0c2..00000000 --- a/apps/web/src/app/api/auth/[...all]/route.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { auth } from "@/auth/server"; -import { toNextJsHandler } from "better-auth/next-js"; - -export const { POST, GET } = toNextJsHandler(auth); diff --git a/apps/web/src/app/api/health/route.ts b/apps/web/src/app/api/health/route.ts deleted file mode 100644 index dbe6fdb1..00000000 --- a/apps/web/src/app/api/health/route.ts +++ /dev/null @@ -1,3 +0,0 @@ -export async function GET() { - return new Response("OK", { status: 200 }); -} diff --git a/apps/web/src/app/api/sounds/search/route.ts b/apps/web/src/app/api/sounds/search/route.ts deleted file mode 100644 index aa7a1b60..00000000 --- a/apps/web/src/app/api/sounds/search/route.ts +++ /dev/null @@ -1,280 +0,0 @@ -import { webEnv } from "@/env/web"; -import { type NextRequest, NextResponse } from "next/server"; -import { z } from "zod"; -import { checkRateLimit } from "@/auth/rate-limit"; - -const searchParamsSchema = z.object({ - q: z.string().max(500, "Query too long").optional(), - type: z.enum(["songs", "effects"]).optional(), - page: z.coerce.number().int().min(1).max(1000).default(1), - page_size: z.coerce.number().int().min(1).max(150).default(20), - sort: z - .enum(["downloads", "rating", "created", "score"]) - .default("downloads"), - min_rating: z.coerce.number().min(0).max(5).default(3), - commercial_only: z.coerce.boolean().default(true), -}); - -const freesoundResultSchema = z.object({ - id: z.number(), - name: z.string(), - description: z.string(), - url: z.string().url(), - previews: z - .object({ - "preview-hq-mp3": z.string().url(), - "preview-lq-mp3": z.string().url(), - "preview-hq-ogg": z.string().url(), - "preview-lq-ogg": z.string().url(), - }) - .optional(), - download: z.string().url().optional(), - duration: z.number(), - filesize: z.number(), - type: z.string(), - channels: z.number(), - bitrate: z.number(), - bitdepth: z.number(), - samplerate: z.number(), - username: z.string(), - tags: z.array(z.string()), - license: z.string(), - created: z.string(), - num_downloads: z.number().optional(), - avg_rating: z.number().optional(), - num_ratings: z.number().optional(), -}); - -const freesoundResponseSchema = z.object({ - count: z.number(), - next: z.string().url().nullable(), - previous: z.string().url().nullable(), - results: z.array(freesoundResultSchema), -}); - -const transformedResultSchema = z.object({ - id: z.number(), - name: z.string(), - description: z.string(), - url: z.string(), - previewUrl: z.string().optional(), - downloadUrl: z.string().optional(), - duration: z.number(), - filesize: z.number(), - type: z.string(), - channels: z.number(), - bitrate: z.number(), - bitdepth: z.number(), - samplerate: z.number(), - username: z.string(), - tags: z.array(z.string()), - license: z.string(), - created: z.string(), - downloads: z.number().optional(), - rating: z.number().optional(), - ratingCount: z.number().optional(), -}); - -const apiResponseSchema = z.object({ - count: z.number(), - next: z.string().nullable(), - previous: z.string().nullable(), - results: z.array(transformedResultSchema), - query: z.string().optional(), - type: z.string(), - page: z.number(), - pageSize: z.number(), - sort: z.string(), - minRating: z.number().optional(), -}); - -function buildSortParameter({ query, sort }: { query?: string; sort: string }) { - if (!query) return `${sort}_desc`; - return sort === "score" ? "score" : `${sort}_desc`; -} - -function applyEffectsFilters({ - params, - min_rating, - commercial_only, -}: { - params: URLSearchParams; - min_rating: number; - commercial_only: boolean; -}) { - params.append("filter", "duration:[* TO 30.0]"); - params.append("filter", `avg_rating:[${min_rating} TO *]`); - - if (commercial_only) { - params.append( - "filter", - 'license:("Attribution" OR "Creative Commons 0" OR "Attribution Noncommercial" OR "Attribution Commercial")', - ); - } - - params.append( - "filter", - "tag:sound-effect OR tag:sfx OR tag:foley OR tag:ambient OR tag:nature OR tag:mechanical OR tag:electronic OR tag:impact OR tag:whoosh OR tag:explosion", - ); -} - -function transformFreesoundResult( - result: z.infer, -) { - return { - id: result.id, - name: result.name, - description: result.description, - url: result.url, - previewUrl: - result.previews?.["preview-hq-mp3"] || - result.previews?.["preview-lq-mp3"], - downloadUrl: result.download, - duration: result.duration, - filesize: result.filesize, - type: result.type, - channels: result.channels, - bitrate: result.bitrate, - bitdepth: result.bitdepth, - samplerate: result.samplerate, - username: result.username, - tags: result.tags, - license: result.license, - created: result.created, - downloads: result.num_downloads || 0, - rating: result.avg_rating || 0, - ratingCount: result.num_ratings || 0, - }; -} - -export async function GET(request: NextRequest) { - try { - const { limited } = await checkRateLimit({ request }); - if (limited) { - return NextResponse.json({ error: "Too many requests" }, { status: 429 }); - } - - const { searchParams } = new URL(request.url); - - const validationResult = searchParamsSchema.safeParse({ - q: searchParams.get("q") || undefined, - type: searchParams.get("type") || undefined, - page: searchParams.get("page") || undefined, - page_size: searchParams.get("page_size") || undefined, - sort: searchParams.get("sort") || undefined, - min_rating: searchParams.get("min_rating") || undefined, - }); - - if (!validationResult.success) { - return NextResponse.json( - { - error: "Invalid parameters", - details: validationResult.error.flatten().fieldErrors, - }, - { status: 400 }, - ); - } - - const { - q: query, - type, - page, - page_size: pageSize, - sort, - min_rating, - commercial_only, - } = validationResult.data; - - if (type === "songs") { - return NextResponse.json( - { - error: "Songs are not available yet", - message: - "Song search functionality is coming soon. Try searching for sound effects instead.", - }, - { status: 501 }, - ); - } - - const baseUrl = "https://freesound.org/apiv2/search/text/"; - - const sortParam = buildSortParameter({ query, sort }); - - const params = new URLSearchParams({ - query: query || "", - token: webEnv.FREESOUND_API_KEY, - page: page.toString(), - page_size: pageSize.toString(), - sort: sortParam, - fields: - "id,name,description,url,previews,download,duration,filesize,type,channels,bitrate,bitdepth,samplerate,username,tags,license,created,num_downloads,avg_rating,num_ratings", - }); - - const isEffectsSearch = type === "effects" || !type; - if (isEffectsSearch) { - applyEffectsFilters({ params, min_rating, commercial_only }); - } - - const response = await fetch(`${baseUrl}?${params.toString()}`); - - if (!response.ok) { - const errorText = await response.text(); - console.error("Freesound API error:", response.status, errorText); - return NextResponse.json( - { error: "Failed to search sounds" }, - { status: response.status }, - ); - } - - const rawData = await response.json(); - - const freesoundValidation = freesoundResponseSchema.safeParse(rawData); - if (!freesoundValidation.success) { - console.error( - "Invalid Freesound API response:", - freesoundValidation.error, - ); - return NextResponse.json( - { error: "Invalid response from Freesound API" }, - { status: 502 }, - ); - } - - const data = freesoundValidation.data; - - const transformedResults = data.results.map(transformFreesoundResult); - - const responseData = { - count: data.count, - next: data.next, - previous: data.previous, - results: transformedResults, - query: query || "", - type: type || "effects", - page, - pageSize, - sort, - minRating: min_rating, - }; - - const responseValidation = apiResponseSchema.safeParse(responseData); - if (!responseValidation.success) { - console.error( - "Invalid API response structure:", - responseValidation.error, - ); - return NextResponse.json( - { error: "Internal response formatting error" }, - { status: 500 }, - ); - } - - return NextResponse.json(responseValidation.data); - } catch (error) { - console.error("Error searching sounds:", error); - return NextResponse.json( - { error: "Internal server error" }, - { status: 500 }, - ); - } -} diff --git a/apps/web/src/app/blog/[slug]/page.tsx b/apps/web/src/app/blog/[slug]/page.tsx deleted file mode 100644 index dba9b504..00000000 --- a/apps/web/src/app/blog/[slug]/page.tsx +++ /dev/null @@ -1,135 +0,0 @@ -import type { Metadata } from "next"; -import Image from "next/image"; -import { notFound } from "next/navigation"; -import { BasePage } from "@/app/base-page"; -import Prose from "@/components/ui/prose"; -import { Separator } from "@/components/ui/separator"; -import { getPosts, getSinglePost, processHtmlContent } from "@/blog/query"; -import type { Author, Post } from "@/blog/types"; - -type PageProps = { - params: Promise<{ slug: string }>; - searchParams: Promise<{ [key: string]: string | string[] | undefined }>; -}; - -export async function generateMetadata({ - params, -}: PageProps): Promise { - const slug = (await params).slug; - - const data = await getSinglePost({ slug }); - - if (!data || !data.post) return {}; - - return { - title: data.post.title, - description: data.post.description, - twitter: { - title: `${data.post.title}`, - description: `${data.post.description}`, - card: "summary_large_image", - images: [ - { - url: data.post.coverImage, - width: "1200", - height: "630", - alt: data.post.title, - }, - ], - }, - openGraph: { - type: "article", - images: [ - { - url: data.post.coverImage, - width: "1200", - height: "630", - alt: data.post.title, - }, - ], - title: data.post.title, - description: data.post.description, - publishedTime: new Date(data.post.publishedAt).toISOString(), - authors: data.post.authors.map((author: Author) => author.name), - }, - }; -} - -export async function generateStaticParams() { - const data = await getPosts(); - if (!data || !data.posts.length) return []; - - return data.posts.map((post) => ({ - slug: post.slug, - })); -} - -export default async function BlogPostPage({ params }: PageProps) { - const slug = (await params).slug; - const data = await getSinglePost({ slug }); - if (!data || !data.post) return notFound(); - - const html = await processHtmlContent({ html: data.post.content }); - - return ( - - - - - - ); -} - -function PostHeader({ post }: { post: Post }) { - const formattedDate = new Date(post.publishedAt).toLocaleDateString("en-US", { - day: "numeric", - month: "long", - year: "numeric", - }); - - return ( -
- - - {post.coverImage && } -
- ); -} - -function PostCoverImage({ post }: { post: Post }) { - return ( -
- {post.title} -
- ); -} - -function PostMeta({ date, publishedAt }: { date: string; publishedAt: Date }) { - return ( -
- -
- ); -} - -function PostTitle({ title }: { title: string }) { - return ( -

- {title} -

- ); -} - -function PostContent({ html }: { html: string }) { - return ( -
- -
- ); -} diff --git a/apps/web/src/app/blog/page.tsx b/apps/web/src/app/blog/page.tsx deleted file mode 100644 index b40bc2fc..00000000 --- a/apps/web/src/app/blog/page.tsx +++ /dev/null @@ -1,52 +0,0 @@ -import type { Metadata } from "next"; -import Link from "next/link"; -import { BasePage } from "@/app/base-page"; -import { Separator } from "@/components/ui/separator"; -import { getPosts } from "@/blog/query"; -import type { Post } from "@/blog/types"; - -export const metadata: Metadata = { - title: "Blog - OpenCut", - description: - "Read the latest news and updates about OpenCut, the free and open-source video editor.", - openGraph: { - title: "Blog - OpenCut", - description: - "Read the latest news and updates about OpenCut, the free and open-source video editor.", - type: "website", - }, -}; - -export default async function BlogPage() { - const data = await getPosts().catch(() => null); - if (!data || !data.posts) return
No posts yet
; - - return ( - -
- {data.posts.map((post) => ( -
- - -
- ))} -
-
- ); -} - -function BlogPostItem({ post }: { post: Post }) { - return ( - -
-
-

{post.title}

-

{post.description}

-
-
- - ); -} diff --git a/apps/web/src/app/changelog/[version]/page.tsx b/apps/web/src/app/changelog/[version]/page.tsx index 6c529ce1..b6623cf3 100644 --- a/apps/web/src/app/changelog/[version]/page.tsx +++ b/apps/web/src/app/changelog/[version]/page.tsx @@ -15,6 +15,8 @@ import { CopyMarkdownButton } from "@/changelog/components/copy-markdown-button" type Props = { params: Promise<{ version: string }> }; +export const dynamicParams = false; + export async function generateStaticParams() { return getSortedReleases().map((release) => ({ version: release.version })); } diff --git a/apps/web/src/app/editor/[project_id]/page.tsx b/apps/web/src/app/editor/[project_id]/page.tsx deleted file mode 100644 index b759b9c2..00000000 --- a/apps/web/src/app/editor/[project_id]/page.tsx +++ /dev/null @@ -1,209 +0,0 @@ -"use client"; - -import { useParams } from "next/navigation"; -import { - ResizablePanelGroup, - ResizablePanel, - ResizableHandle, -} from "@/components/ui/resizable"; -import { AssetsPanel } from "@/components/editor/panels/assets"; -import { PropertiesPanel } from "@/components/editor/panels/properties"; -import { Timeline } from "@/timeline/components"; -import { PreviewPanel } from "@/preview/components"; -import { EditorHeader } from "@/components/editor/editor-header"; -import { EditorProvider } from "@/components/providers/editor-provider"; -import { Onboarding } from "@/components/editor/onboarding"; -import { MigrationDialog } from "@/project/components/migration-dialog"; -import { usePanelStore } from "@/editor/panel-store"; -import { usePasteMedia } from "@/media/use-paste-media"; -import { MobileGate } from "@/components/editor/mobile-gate"; -import { useMemo, useState } from "react"; -import { useEditor } from "@/editor/use-editor"; -import { Cancel01Icon } from "@hugeicons/core-free-icons"; -import { HugeiconsIcon } from "@hugeicons/react"; -import { Button } from "@/components/ui/button"; -import { ChangelogNotification } from "@/changelog/components/changelog-notification"; -import { - createPreviewOverlayControl, - isPreviewOverlayVisible, - mergePreviewOverlaySources, -} from "@/preview/overlays"; -import { usePreviewStore } from "@/preview/preview-store"; -import { getGuidePreviewOverlaySource } from "@/guides"; -import { - bookmarkNotesPreviewOverlay, - getBookmarkPreviewOverlaySource, -} from "@/timeline/bookmarks/index"; - -export default function Editor() { - const params = useParams<{ project_id: string }>(); - const projectId = params.project_id; - - return ( - - -
- - -
- -
- - - -
-
-
- ); -} - -function DegradedRendererBanner() { - const isDegraded = useEditor((e) => e.renderer.isDegraded); - const [dismissed, setDismissed] = useState(false); - if (!isDegraded || dismissed) return null; - - return ( -
- For the best experience, open OpenCut in Chrome. - -
- ); -} - -function EditorLayout() { - usePasteMedia(); - const { panels, setPanel } = usePanelStore(); - const activeScene = useEditor((editor) => - editor.scenes.getActiveSceneOrNull(), - ); - const currentTime = useEditor((editor) => editor.playback.getCurrentTime()); - const activeGuide = usePreviewStore((state) => state.activeGuide); - const overlays = usePreviewStore((state) => state.overlays); - const setOverlayVisibility = usePreviewStore( - (state) => state.setOverlayVisibility, - ); - const showBookmarkNotes = isPreviewOverlayVisible({ - overlay: bookmarkNotesPreviewOverlay, - overlays, - }); - - const overlaySource = useMemo( - () => - mergePreviewOverlaySources({ - sources: [ - getGuidePreviewOverlaySource({ - guideId: activeGuide, - }), - activeScene - ? getBookmarkPreviewOverlaySource({ - bookmarks: activeScene.bookmarks, - time: currentTime, - isVisible: showBookmarkNotes, - }) - : { - definitions: [bookmarkNotesPreviewOverlay], - instances: [], - }, - ], - }), - [activeGuide, activeScene, currentTime, showBookmarkNotes], - ); - - const overlayControls = useMemo( - () => - overlaySource.definitions.map((overlay) => - createPreviewOverlayControl({ overlay, overlays }), - ), - [overlaySource.definitions, overlays], - ); - - return ( - { - setPanel({ - panel: "mainContent", - size: sizes[0] ?? panels.mainContent, - }); - setPanel({ - panel: "timeline", - size: sizes[1] ?? panels.timeline, - }); - }} - > - - { - setPanel({ panel: "tools", size: sizes[0] ?? panels.tools }); - setPanel({ panel: "preview", size: sizes[1] ?? panels.preview }); - setPanel({ - panel: "properties", - size: sizes[2] ?? panels.properties, - }); - }} - > - - - - - - - - - - - - - - - - - - - - - - - - - ); -} diff --git a/apps/web/src/app/editor/page.tsx b/apps/web/src/app/editor/page.tsx new file mode 100644 index 00000000..8229774a --- /dev/null +++ b/apps/web/src/app/editor/page.tsx @@ -0,0 +1,217 @@ +"use client"; + +import { useSearchParams } from "next/navigation"; +import { + ResizablePanelGroup, + ResizablePanel, + ResizableHandle, +} from "@/components/ui/resizable"; +import { AssetsPanel } from "@/components/editor/panels/assets"; +import { PropertiesPanel } from "@/components/editor/panels/properties"; +import { Timeline } from "@/timeline/components"; +import { PreviewPanel } from "@/preview/components"; +import { EditorHeader } from "@/components/editor/editor-header"; +import { EditorProvider } from "@/components/providers/editor-provider"; +import { Onboarding } from "@/components/editor/onboarding"; +import { MigrationDialog } from "@/project/components/migration-dialog"; +import { usePanelStore } from "@/editor/panel-store"; +import { usePasteMedia } from "@/media/use-paste-media"; +import { MobileGate } from "@/components/editor/mobile-gate"; +import { Suspense, useMemo, useState } from "react"; +import { useEditor } from "@/editor/use-editor"; +import { Cancel01Icon } from "@hugeicons/core-free-icons"; +import { HugeiconsIcon } from "@hugeicons/react"; +import { Button } from "@/components/ui/button"; +import { ChangelogNotification } from "@/changelog/components/changelog-notification"; +import { + createPreviewOverlayControl, + isPreviewOverlayVisible, + mergePreviewOverlaySources, +} from "@/preview/overlays"; +import { usePreviewStore } from "@/preview/preview-store"; +import { getGuidePreviewOverlaySource } from "@/guides"; +import { + bookmarkNotesPreviewOverlay, + getBookmarkPreviewOverlaySource, +} from "@/timeline/bookmarks/index"; + +export default function Editor() { + return ( + + + + ); +} + +function EditorView() { + const searchParams = useSearchParams(); + const projectId = searchParams.get("project") ?? ""; + + return ( + + +
+ + +
+ +
+ + + +
+
+
+ ); +} + +function DegradedRendererBanner() { + const isDegraded = useEditor((e) => e.renderer.isDegraded); + const [dismissed, setDismissed] = useState(false); + if (!isDegraded || dismissed) return null; + + return ( +
+ For the best experience, open OpenCut in Chrome. + +
+ ); +} + +function EditorLayout() { + usePasteMedia(); + const { panels, setPanel } = usePanelStore(); + const activeScene = useEditor((editor) => + editor.scenes.getActiveSceneOrNull(), + ); + const currentTime = useEditor((editor) => editor.playback.getCurrentTime()); + const activeGuide = usePreviewStore((state) => state.activeGuide); + const overlays = usePreviewStore((state) => state.overlays); + const setOverlayVisibility = usePreviewStore( + (state) => state.setOverlayVisibility, + ); + const showBookmarkNotes = isPreviewOverlayVisible({ + overlay: bookmarkNotesPreviewOverlay, + overlays, + }); + + const overlaySource = useMemo( + () => + mergePreviewOverlaySources({ + sources: [ + getGuidePreviewOverlaySource({ + guideId: activeGuide, + }), + activeScene + ? getBookmarkPreviewOverlaySource({ + bookmarks: activeScene.bookmarks, + time: currentTime, + isVisible: showBookmarkNotes, + }) + : { + definitions: [bookmarkNotesPreviewOverlay], + instances: [], + }, + ], + }), + [activeGuide, activeScene, currentTime, showBookmarkNotes], + ); + + const overlayControls = useMemo( + () => + overlaySource.definitions.map((overlay) => + createPreviewOverlayControl({ overlay, overlays }), + ), + [overlaySource.definitions, overlays], + ); + + return ( + { + setPanel({ + panel: "mainContent", + size: sizes[0] ?? panels.mainContent, + }); + setPanel({ + panel: "timeline", + size: sizes[1] ?? panels.timeline, + }); + }} + > + + { + setPanel({ panel: "tools", size: sizes[0] ?? panels.tools }); + setPanel({ panel: "preview", size: sizes[1] ?? panels.preview }); + setPanel({ + panel: "properties", + size: sizes[2] ?? panels.properties, + }); + }} + > + + + + + + + + + + + + + + + + + + + + + + + + + ); +} diff --git a/apps/web/src/app/layout.tsx b/apps/web/src/app/layout.tsx index 558ae538..9713d9d5 100644 --- a/apps/web/src/app/layout.tsx +++ b/apps/web/src/app/layout.tsx @@ -5,21 +5,12 @@ import { Toaster } from "../components/ui/sonner"; import { ChangelogNotification } from "@/changelog/components/changelog-notification"; import { TooltipProvider } from "../components/ui/tooltip"; import { baseMetaData } from "./metadata"; -import { BotIdClient } from "botid/client"; -import { webEnv } from "@/env/web"; import { Inter } from "next/font/google"; const siteFont = Inter({ subsets: ["latin"] }); export const metadata = baseMetaData; -const protectedRoutes = [ - { - path: "/none", - method: "GET", - }, -]; - export default function RootLayout({ children, }: Readonly<{ @@ -28,7 +19,6 @@ export default function RootLayout({ return ( - {process.env.NODE_ENV === "development" && ( <>