From fedf1d24c029cfa12a524cb7f934fab830555aa3 Mon Sep 17 00:00:00 2001 From: Twest2 Date: Fri, 3 Jul 2026 23:26:44 -0500 Subject: [PATCH] Add production .deb packaging, apt setup, desktop integration, and icons MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 3 of the improvement plan (PR 8 of the sequence): the apt/X11 packaging half of Linux support, in separate Linux-specific files. Replaces the old scripts/package-linux.sh, which the audit flagged as "not production packaging" (it copied the whole dev node_modules — including vulnerable build deps — plus docs/prompts/examples/audit files, hardcoded amd64, declared only xinput, lacked desktop/icon/MIME integration, and could build without node_modules). Production builder (packaging/linux/debian/package.sh): - Stages ONLY runtime files: app code, a fixed Electron runtime, and the production npm deps (enumerated via npm ls --omit=dev). Never copies the development node_modules; guards against electron-builder/app-builder-lib leaking in. Fails if node_modules is absent instead of shipping an unusable artifact. - Detects architecture (dpkg --print-architecture, x64/arm64) rather than hardcoding amd64. Generates DEBIAN/control from control.in with proper runtime Depends, real maintainer, and homepage. - Installs a desktop entry, hicolor icons (16–512), .sfgz/.sfglt MIME registration, the launcher, and the license. postinst makes chrome-sandbox setuid and refreshes desktop/MIME/icon caches; postrm cleans them. - Emits a .deb, a portable tarball that now INCLUDES /usr/bin/stepforge (the old tarball omitted it), and a sha256 sums file. Launcher (packaging/linux/common/launcher.sh): - Runs sandboxed; prefers the user-namespace sandbox, accepts a root-owned setuid helper, and otherwise refuses to launch with an actionable message. --no-sandbox requires an explicit STEPFORGE_ALLOW_NO_SANDBOX opt-in. Never installs anything at runtime. Setup (separate build vs runtime, apt only): - scripts/linux/apt/install-runtime-deps.sh (Chromium/Electron libs, X11 tools, portal/PipeWire) and install-build-deps.sh (dpkg-dev, fakeroot, xvfb). Runtime script installs no build tools. Assets: original StepForge icon — packaging/assets/stepforge.svg plus a generator (scripts/make-icons.js) that renders the PNG set with the repo's own rasterizer/PNG writer (no third-party art). npm run icons regenerates them. Wiring: package.json gains package:linux:deb / package:linux:rpm / icons; build-release.sh uses the production builder and requires node_modules; README points at the apt/dnf guides. Tests: tests/unit/packaging-linux.test.js (structural: files present in their separate locations, old script gone, valid desktop entry, templated arch + runtime Depends, launcher gates --no-sandbox, builder requires node_modules and guards dev-dep leaks, apt build/runtime dep separation, original icon set generates a valid PNG) runs in the normal suite; tests/integration/linux/package-deb.test.sh builds a real .deb and asserts the right files present and the dev tree / build tooling / app docs absent (honest skip only when dpkg-deb/node_modules are genuinely missing). Verified locally: 276 unit tests pass; the integration test builds and validates stepforge_0.3.2_amd64.deb; build-release E2E passes with the new production package. Co-Authored-By: Claude Fable 5 --- README.md | 7 +- docs/linux/apt.md | 67 +++++++++ package.json | 3 + packaging/assets/icons/stepforge-128.png | Bin 0 -> 1093 bytes packaging/assets/icons/stepforge-16.png | Bin 0 -> 234 bytes packaging/assets/icons/stepforge-256.png | Bin 0 -> 2176 bytes packaging/assets/icons/stepforge-32.png | Bin 0 -> 352 bytes packaging/assets/icons/stepforge-48.png | Bin 0 -> 482 bytes packaging/assets/icons/stepforge-512.png | Bin 0 -> 4654 bytes packaging/assets/icons/stepforge-64.png | Bin 0 -> 611 bytes packaging/assets/icons/stepforge.png | Bin 0 -> 2176 bytes packaging/assets/stepforge.svg | 23 ++++ packaging/linux/common/launcher.sh | 71 ++++++++++ packaging/linux/common/stepforge-mime.xml | 13 ++ packaging/linux/common/stepforge.desktop | 13 ++ packaging/linux/debian/control.in | 17 +++ packaging/linux/debian/package.sh | 143 ++++++++++++++++++++ scripts/build-release.sh | 9 +- scripts/linux/apt/install-build-deps.sh | 32 +++++ scripts/linux/apt/install-runtime-deps.sh | 31 +++++ scripts/make-icons.js | 69 ++++++++++ scripts/package-linux.sh | 92 ------------- tests/integration/linux/package-deb.test.sh | 78 +++++++++++ tests/unit/packaging-linux.test.js | 97 +++++++++++++ 24 files changed, 671 insertions(+), 94 deletions(-) create mode 100644 docs/linux/apt.md create mode 100644 packaging/assets/icons/stepforge-128.png create mode 100644 packaging/assets/icons/stepforge-16.png create mode 100644 packaging/assets/icons/stepforge-256.png create mode 100644 packaging/assets/icons/stepforge-32.png create mode 100644 packaging/assets/icons/stepforge-48.png create mode 100644 packaging/assets/icons/stepforge-512.png create mode 100644 packaging/assets/icons/stepforge-64.png create mode 100644 packaging/assets/icons/stepforge.png create mode 100644 packaging/assets/stepforge.svg create mode 100755 packaging/linux/common/launcher.sh create mode 100644 packaging/linux/common/stepforge-mime.xml create mode 100644 packaging/linux/common/stepforge.desktop create mode 100644 packaging/linux/debian/control.in create mode 100755 packaging/linux/debian/package.sh create mode 100755 scripts/linux/apt/install-build-deps.sh create mode 100755 scripts/linux/apt/install-runtime-deps.sh create mode 100644 scripts/make-icons.js delete mode 100644 scripts/package-linux.sh create mode 100644 tests/integration/linux/package-deb.test.sh create mode 100644 tests/unit/packaging-linux.test.js diff --git a/README.md b/README.md index 02c1dc3..eda56ba 100644 --- a/README.md +++ b/README.md @@ -73,7 +73,12 @@ using only Node built-ins. For a Windows installation, see [docs/windows_installation](docs/windows_installation.md) or for a developer/more in depth walkthrough, see [docs/GETTING_STARTED.md](docs/GETTING_STARTED.md). -On **Linux** (⚠️ work in progress — X11 vs Wayland, enabling per-click capture, the screen-share prompt), see [docs/GETTING_STARTED_WITH_LINUX.md](docs/GETTING_STARTED_WITH_LINUX.md). +On **Linux**, install from a package built for your distro family: +apt-based (Debian/Ubuntu) → [docs/linux/apt.md](docs/linux/apt.md); dnf-based +(Fedora) → [docs/linux/dnf.md](docs/linux/dnf.md). Wayland uses the XDG portal +for screen capture and a hotkey/interval trigger (per-click capture with a +marker needs X11 + xinput). The general developer walkthrough is +[docs/GETTING_STARTED_WITH_LINUX.md](docs/GETTING_STARTED_WITH_LINUX.md). Requirements: Node.js 22.12+ and npm (pinned in `.nvmrc`; installs are refused on older Nodes because the packaging toolchain needs 22.12+). diff --git a/docs/linux/apt.md b/docs/linux/apt.md new file mode 100644 index 0000000..a126459 --- /dev/null +++ b/docs/linux/apt.md @@ -0,0 +1,67 @@ +# StepForge on apt-based Linux (Debian / Ubuntu) + +This is the setup and packaging guide for **apt-based** distributions. Fedora +and other dnf-based systems have a separate guide: [dnf.md](dnf.md). + +## Install from the .deb + +```bash +sudo apt install ./stepforge__amd64.deb +``` + +apt pulls the required runtime libraries automatically (they are declared as +`Depends`). The package installs: + +- the app and a fixed Electron runtime under `/opt/stepforge`, +- the `stepforge` launcher at `/usr/bin/stepforge`, +- a desktop entry, icons, and `.sfgz`/`.sfglt` file associations. + +Launch it from your application menu or run `stepforge`. + +### Sandbox + +The launcher runs **sandboxed**. On most modern kernels the Chromium +user-namespace sandbox works out of the box; the package's `postinst` also +makes the setuid `chrome-sandbox` helper usable as a fallback. StepForge will +**not** silently launch unsandboxed — see the launcher's message if the +sandbox is unavailable. + +## Install from the portable tarball + +```bash +tar -xzf stepforge__linux-x64.tar.gz +# Install the runtime libraries first (see below), then run: +./usr/bin/stepforge # or move opt/stepforge to /opt and use the launcher +``` + +The tarball includes the `/usr/bin/stepforge` launcher (unlike older builds). +Install the runtime libraries with: + +```bash +bash scripts/linux/apt/install-runtime-deps.sh +``` + +## Capture capabilities on apt systems + +- **X11**: full per-click capture with an accurate marker (needs `xinput`). +- **Wayland**: screen capture via the XDG Desktop Portal + PipeWire; the + portal asks permission once per recording. Per-click capture with + coordinates is not exposed by Wayland, so recording uses a global hotkey or + interval trigger. StepForge reports the active trigger honestly. + +Run StepForge and open Settings → Diagnostics to see the detected session +type, portal/PipeWire status, and the active capture profile. + +## Build the .deb yourself + +```bash +bash scripts/linux/apt/install-build-deps.sh # dpkg-dev, fakeroot, xvfb, … +nvm install && nvm use # pinned Node 22 (see .nvmrc) +npm ci +npm run package:linux:deb # -> build/artifacts/*.deb + tarball + sha256 +``` + +The builder stages **only** runtime files: the app code, a fixed Electron +runtime, and production npm dependencies. It never copies the development +`node_modules`, docs, prompts, or examples, and it fails if `node_modules` is +missing rather than producing an unusable artifact. diff --git a/package.json b/package.json index afd39a4..b2c9649 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,10 @@ "start": "node scripts/start-electron.js", "test": "node scripts/run-unit-tests.js", "sample": "node scripts/make-sample-guide.js", + "icons": "node scripts/make-icons.js", "package:windows": "node scripts/package-windows.js", + "package:linux:deb": "bash packaging/linux/debian/package.sh", + "package:linux:rpm": "bash packaging/linux/fedora/package.sh", "build": "bash scripts/build-release.sh", "verify": "bash scripts/verify.sh", "bootstrap": "bash scripts/bootstrap-offline.sh" diff --git a/packaging/assets/icons/stepforge-128.png b/packaging/assets/icons/stepforge-128.png new file mode 100644 index 0000000000000000000000000000000000000000..eedbb053aecb6089182ce8c84e7088c81a0d175d GIT binary patch literal 1093 zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H1|$#LC7xzrVBzs}aSW-L^Y+rsqR>zYmIrm5 z`RDIFe^VRh+106bn})S0s6K6-P~UH0{`w~~%353Z{~ZptA7vq*=U=*& zY2DzsPxk5`^(|$$_!A29-Ahj%-%=LEpU{y1ox!G|KjZBd`2!E`^<}+tj;hwyt3UAG z&Gb9t+Pzcx6E*;u%@kMZ4Iqt}D+x`;u+JPWG!u-(Q$Dnb)DDpZx*j9^Y&~ zbB43FFW%O4Uz?+~s`|q1pK5C@?=qNtuV8q{Zt$6Ht>sMym+uSAKU+bB7nm2%xi%+{ zVOI5p*>--}er608ZD08AcY~VayWb1Siv7X`(ObbFci{AvXC=%R?lj-)n?9p^&Z1w& zU&{8JhHJOid(vVO_w_`!Og7U-L~|Z zV2vf)Z4X0~?tYsIQF@nqev)2M*Q@h34T`-54E{!7eJ}EAn|7YiTxIWE{`&D5SHHl& zGuaK6T;xqSoebtl?uwRM+B8`#RR2Yur5mfd?y9&8cWZjM{GvkkF4+B3P|G4S@N3h% zXYDH=EOEWcy-VE21*`~S34hQ*&+b*?Q)bI80Gpv8`--2}=WtO<~ zg3X{#8mh2P8^SQGn*s5PgtbpM_t6`=z$D#s7sPCvol#iHaB$&`0%cu2k=OYPjmiHUcWwr3PBj3}P~WL|6r>5KCQ>U&`dV#}cz`6QRJ| z7+I|f&W$D4;vBoV=bBCj=En51sgT@g`9CNsrk*L_>AJM#|F^XRiw6czS3j3^P6b$Vbe4r||ZzMon`1uI4Sfmv#phykYW8KP=&KEBlzk d=B-IR{Ojc=$f}t6P6axi!PC{xWt~$(69B=KUgH1& literal 0 HcmV?d00001 diff --git a/packaging/assets/icons/stepforge-256.png b/packaging/assets/icons/stepforge-256.png new file mode 100644 index 0000000000000000000000000000000000000000..838010972002fae2dfa5a8ef35e095b96825a790 GIT binary patch literal 2176 zcmZ`*c~BE~6#lI%2{_>rtAIkV)`8ZdP{0EUL?s+WK~S-6s8A!3(NIJ{kmkZVg39I8 zGD-?E=pccyjA!!Fbnp}6tk3oe%)&dOlk@)h zY01e$zc}%eEu>4+s7qH^!?*xz_gE4k8-kadx3$_wg5Ef!KP!y;ZOymqEvkNI)@oQt zWgnwin8Bz{9YWTS!pk)fNTCn43%xldwxHDg<=uCiEJ~emAU+2I_IGYlG*Xap2yjG( zg#RdkRG|B-@zfqzH?tfIhQTW~f(uYQ?&^DA9RvR>ac&O>tRzrJAOvXbU5XwFd zeBA+wN&qdFZ*Ee*g2o>JklUqv)}*H~x~75Gp@wmuw6BUuF>tB{!)2&i`zlu7>)_e9qH%;NX5@Y(@kq*hRT8mh}12_*Vb-H0$}RD)PvU7Jp=_S4>j z_mi1ZTF1;KTHbOTh$FM7a_ZQ8`DBdGp55(@TBT{fmTE!(j~{6!>@qvtflFMCA1kmY zhR)P%!&i1{pcd_s^(c|9Xnblql?)eBR;0rwII!Lg*b~4vvA~n=txCpA#^F-7 z4v!liN%ZG7`wfvy?xMk9o|Q>>WxWqGfMLUXw(j#y_eK zlz!=_VQemDi83qe{WZFibHlWf%*U_&SZX%!EV`=uS;Tzw@gRuUQ2yK>+bha=fxgD) zyc-UraNR51eWT=rZsoyV-w!HsM8K&no`-l8p>T>*lOc4zIvcX3q{7xn;So<9;01=N zOImHgb$eI&&w+Oa-r86kw$p`FP;m55iOf!1ZWF$>AWk1fIkVo*W71P!XU${eLZnK4 zcZ!2H6aLOvplw9vW9T+hHa!GAbt(zZ&nZ;;UYkA%t%{nDm5ZYq1$_&|r8SF-tMlfG z&m1W#Ncdqvt8sTsyle1ZYF=e zF6WPy3ogW0dyiINxvs(CYZ#SQ)qtc(cIw%KJ6=pdmVL6H;;{iscpBk>3e~9TZx<-2% z=I1Dm>76swo(&7r6Z}Qbe{Udke*LUe`6gv>_E&q6G*28SK5$SKX3NQ9d=_wpC-W(C zMU(~kXIwnw%pfYrrd=1v&Oj5VjgUkf2XgvRPkX8|YLrLhtoH9)xAyTaCW3yzWp+Fn z7m5YJmz4q=T+bc9p+2LIr zv#<-t{N`8~C3*}pBt$wRI)rp%zOz#GN;b!4q%}?G&3B8X4}6&3YT`t3doe9Nz{Cy`#vE^n(&qCkRF(2X`Q3p literal 0 HcmV?d00001 diff --git a/packaging/assets/icons/stepforge-32.png b/packaging/assets/icons/stepforge-32.png new file mode 100644 index 0000000000000000000000000000000000000000..5777d99f23c763b424afc696d9836924dfe3871e GIT binary patch literal 352 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE1|*BCs=hEVFsggHIEGZrIePB4n`@#7>w}L% zci)x%xL(y&67X`tB}ewDe6K2%AYfNVM~8?Y5L}clF;*%wH~qKwy3x$Z@AJ-CvYqPn zuX5V?&(UAw`n{K&M_)y`N`sm@8*RiEzT22%ghW#5@+`iAMOsGCC%*+nwcF!|NR}3#p?R+FT3Y`Ia|e-cbBTG*jj&S pSyDZF-h~&p7O9(9nZJA~9e&A* zcisK}_@Btt5<6ex%bDeN*RM+|pY)hCQAJYOQ^|AEl1VC_lT?zw@a~Gd>uS6$(t7>> z*Ofo+u1nW_qCP)E@{@2qka)a5Q}R=C{}!H?H<)#ut*ZP_Pw2aI(P+ux?LeUmytS2f zn$?eOB|X^JzdU^ZX}tTC8OG4bdh4V#5)n^JXt(WWn zpHlD6HtC*mRNIp0QzC=?PdZ(yJ+tN{bBm5jt8$vJoI;ACo{UlUE6vD`-F!1lD;p-X z?kJugIhma!vcvitRG@bHx*c{;)gNt>-Sw!Z;e&ADZL`=y`v?E+1#53E(q8BPv-8Dg zwb(*Chaa6UF0(-xD@x~b$uIxn^rf(uOP{$Vt|O{0@L_7h1@6St{#rG2E^#M*))I|l zm#vOyFtDnm{r-UW| DVzkuB literal 0 HcmV?d00001 diff --git a/packaging/assets/icons/stepforge-512.png b/packaging/assets/icons/stepforge-512.png new file mode 100644 index 0000000000000000000000000000000000000000..38219b99ca0c8389d09be044553087e93d3e5ee3 GIT binary patch literal 4654 zcmbVQYgAKL7TzH#h^UCzWg*tkg3uWiMtOyC$;E;LREgGUi&GJhf=~$sd=y(C3F8b_ zoe086kRpN6S(G{j8Y|!fWQqm^<*Aj3W2mX(BMKNufIRM;0C`{ni}>rFbzg62<`Po>J-uy6NvCW2+3pWJVY&o;oDu7^>Moi3HQuGTo zyWKsOCwrRSekCGasN8*)?Zan=rFY?*pqO)9Nd#7dJ3w(s@^FaoB>1@@ZjWdHKY#VW zquf_^$`Xdwps&yt9$y1sWzGYLYnn3P-|kbzn?dbc0PWcX6pfu`z&@L(XhRO7G#>R_ z*9~IM(0+6Zeu|-#1YfV7&?ZRS48==`0QPG%6Cslm_I+)PDdcQP>=hpZD}0L1xI#>F zQyobwcT{Fn(6};@;w1CiSF@m5NmQpSSvBNsAe${I#rGRefTZ+_uf7=m=$|9F`V1mP z8Pg!~layu5B7LZ~HX_A!W`WU_@v60Q6^)klzbg7;nb91vu*KSi2)#Ma(+B38E%+}$ zjnfk40NE_e3C)`Ake*JQm-OL0U|Ho1kB|wM7bE8tArO91d%1 zBK-^}2Kr^-nRgMK;~>xQCLOqdhNpOx{f?}okpSXsWj>h@_D^JiZG!MHsiW#;pr48v z^tL(Fn+vk4ZR$6m?dH|QsF@G01q2v)mq$r(WVIk$5|0v(MyNs~Pf5LhkHKznyx}Fj zeGl3cn4i=!fs}L-kuAwqQW1q068}atcEcPv35A&Iwq?DnnUQbp1#=a5e)|JJ-Dc@& zIw@(6NHJKk3F=nJ*ZV4<32*bcoGcv)bq3r`zPdF>d1dbbq-;Nr5=CVBqs>;Ht6WHi zrAG*mbV_`v8+y0!1)_)|Nb^zJwB{-Cz1tAnQ(&D`CC0f8Xfu{yXRyx2;P}!!WD+7J z7Os-xeU(UOJde^#7vR_-&(>w~PEzm2C#D$10EFc#A+|+@`>aHv7KkmV+ogEhPWHLR zIx1l#YD~xUEE@ZH8FK+ToHaD5KOK_ZGZs=aHC1e7%j$1t{KFa_>BIY$7NPW~^C;0M z^0#8M7(2}dYI&502iK|IP{r>-XW7j`zS9e8=*XK@6C)oLtcRL9)b9dq93?eECF0_g z+(v*o(OBm)rYkzg3!50 zfvPXe20CLfX&X}^!EK+SUFG`CA?^v}U`dmx?e_@0@j*xF8uiZqYNRZ#aNWylTULVr z@9o2PF-8#V)vM}UpszPNseC~L*We29XkDffPhLmwOk6{(n6fVgeFyUiuyW)17GKKM zc@X)R?{`Q?9lQ|u25H;z-JEpaZ|tC+_t7P>WIerb-Ox$P%6D(O%F`CQgMm3;;Lmg? zd~)V)4A(l!94c2%2h`^o8#3Uz0g(6aX`FOZQwO?iBR5RQq)`;lW)P(1*#*$D^*OK2 z;>YB%-iQ#^o6`lZ1+FH-w!HE1*iNMBPnIS4M1_-b(9UC!5`MhZ$w|pP;F=Hv-hgM=$gpIIN*2ms%@E zO)Oq|Z<}-dk@4N6%hgQj;Xff#xCRMF%G1-#dV=nTOJyJ20M^@132yS#<-4GMK~5D% zS-Xf_S|RVUq&t3|DiE$f5r3KeQ?0oF1lsCrr#EtkNQ>Z}7FGCRwSW4|{$)%f054iU zo@**Cxm1P^lB$lIq|gp351x4*!>qWNaU*D%8Vn2}(RYnO`A$1~~o zqJ>K=Y`g!|Ddk-WMHhqE!KAW~k^iQd{lAbK0niKY$>gTedeL6UYfY}8i!A+sCY$ua z%VQ0u(}pWA>5Tl6bAH+_kh!cT7E^;iJujB$<`cFQI{omeR2h5&-lOGwvP!OULU~n| z1n+ni+D*M6^UuBL^!aUG|K=!jLtkn@iu3;9&hKM{+^h3g52lA2r+uO9J>xpPu3fO$ zCHQP!I&%@(MV6Mdl`|n6)kD%VldfsVMt?Aj(HK;Zwo#TAsij>iRA!j&Aq9UlyBOW! zNbZ@BTk5dGY|$klRKe@5uYvJs)k{eTM6Ey

Q5k0O-p{X0reLZfd@zgfIlfR$pmVIAO zn|@oYU76&oR^6NW%c@mU{p!!GZl&%s{XUj`di(v=Q`*;#RhYZ}gtp7(cdSaB(%71- z9}tdx!H^>nc7fjb&@^Ja@moe$COZ9nk{;AIQQ8ODecqGuiYQ%+I7<4h{^3{(M?KOKEvWzCZB>7<`#h`J}d+)e6@CeCS pQ-1luO~Xh|geo3SW}yTpHk5Ds&R+E}6}0aYL~8LZ)>if|3B8LyLjTVxQn}v&ifzSRA;tI zS3fg$GgH*e-mFioYqO&qxHkv-+AS|V%YEUz+t#0ww+}8lzuiLo>XG1W-T&vsH_cq3 zHt&LQ$m9N(lB+(pZ}AX5D;F|b?t)S%W0!45^7qV%YO_Fe#>90DKjn_}mS;{}#c)*a z$VKZd9-$3M{EuGbMlI2H*vDRYST|~kmcvH&!Y69QCj`&Nb|{pHXx-DOco)O@v4pl+1)m?eadO=*`%*TYyd9s9lFu7;M7D;weG1BJO%8$1gRS$$t@GzCd~wVDd_c*&rRfrtAIkV)`8ZdP{0EUL?s+WK~S-6s8A!3(NIJ{kmkZVg39I8 zGD-?E=pccyjA!!Fbnp}6tk3oe%)&dOlk@)h zY01e$zc}%eEu>4+s7qH^!?*xz_gE4k8-kadx3$_wg5Ef!KP!y;ZOymqEvkNI)@oQt zWgnwin8Bz{9YWTS!pk)fNTCn43%xldwxHDg<=uCiEJ~emAU+2I_IGYlG*Xap2yjG( zg#RdkRG|B-@zfqzH?tfIhQTW~f(uYQ?&^DA9RvR>ac&O>tRzrJAOvXbU5XwFd zeBA+wN&qdFZ*Ee*g2o>JklUqv)}*H~x~75Gp@wmuw6BUuF>tB{!)2&i`zlu7>)_e9qH%;NX5@Y(@kq*hRT8mh}12_*Vb-H0$}RD)PvU7Jp=_S4>j z_mi1ZTF1;KTHbOTh$FM7a_ZQ8`DBdGp55(@TBT{fmTE!(j~{6!>@qvtflFMCA1kmY zhR)P%!&i1{pcd_s^(c|9Xnblql?)eBR;0rwII!Lg*b~4vvA~n=txCpA#^F-7 z4v!liN%ZG7`wfvy?xMk9o|Q>>WxWqGfMLUXw(j#y_eK zlz!=_VQemDi83qe{WZFibHlWf%*U_&SZX%!EV`=uS;Tzw@gRuUQ2yK>+bha=fxgD) zyc-UraNR51eWT=rZsoyV-w!HsM8K&no`-l8p>T>*lOc4zIvcX3q{7xn;So<9;01=N zOImHgb$eI&&w+Oa-r86kw$p`FP;m55iOf!1ZWF$>AWk1fIkVo*W71P!XU${eLZnK4 zcZ!2H6aLOvplw9vW9T+hHa!GAbt(zZ&nZ;;UYkA%t%{nDm5ZYq1$_&|r8SF-tMlfG z&m1W#Ncdqvt8sTsyle1ZYF=e zF6WPy3ogW0dyiINxvs(CYZ#SQ)qtc(cIw%KJ6=pdmVL6H;;{iscpBk>3e~9TZx<-2% z=I1Dm>76swo(&7r6Z}Qbe{Udke*LUe`6gv>_E&q6G*28SK5$SKX3NQ9d=_wpC-W(C zMU(~kXIwnw%pfYrrd=1v&Oj5VjgUkf2XgvRPkX8|YLrLhtoH9)xAyTaCW3yzWp+Fn z7m5YJmz4q=T+bc9p+2LIr zv#<-t{N`8~C3*}pBt$wRI)rp%zOz#GN;b!4q%}?G&3B8X4}6&3YT`t3doe9Nz{Cy`#vE^n(&qCkRF(2X`Q3p literal 0 HcmV?d00001 diff --git a/packaging/assets/stepforge.svg b/packaging/assets/stepforge.svg new file mode 100644 index 0000000..29493c3 --- /dev/null +++ b/packaging/assets/stepforge.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + diff --git a/packaging/linux/common/launcher.sh b/packaging/linux/common/launcher.sh new file mode 100755 index 0000000..e33e9c3 --- /dev/null +++ b/packaging/linux/common/launcher.sh @@ -0,0 +1,71 @@ +#!/usr/bin/env sh +# StepForge launcher installed at /usr/bin/stepforge. +# +# Runs the packaged Electron runtime against the installed app at +# /opt/stepforge. It NEVER installs or repairs anything at runtime and it does +# NOT silently disable the Chromium sandbox: an unsandboxed launch requires the +# explicit STEPFORGE_ALLOW_NO_SANDBOX=1 opt-in (development/CI only). + +set -eu + +APP_DIR=/opt/stepforge +ELECTRON="$APP_DIR/node_modules/electron/dist/electron" +SANDBOX_HELPER="$APP_DIR/node_modules/electron/dist/chrome-sandbox" + +if [ ! -x "$ELECTRON" ]; then + echo "stepforge: Electron runtime missing at $ELECTRON (reinstall the package)." >&2 + exit 1 +fi + +cd "$APP_DIR" || exit 1 + +# Linux screen capture: enable the PipeWire path for Wayland portals; harmless +# on X11 where Ozone auto-selects. +COMMON_ARGS="--enable-features=WebRTCPipeWireCapturer --ozone-platform-hint=auto" + +sandbox_ok() { + [ -e "$SANDBOX_HELPER" ] || return 1 + helper_uid="$(stat -c '%u' "$SANDBOX_HELPER" 2>/dev/null || echo '')" + helper_mode="$(stat -c '%a' "$SANDBOX_HELPER" 2>/dev/null || echo '')" + [ "$helper_uid" = "0" ] || return 1 + [ -n "$helper_mode" ] || return 1 + # setuid bit set? + [ $(( $((8#$helper_mode)) & 04000 )) -ne 0 ] || return 1 + return 0 +} + +userns_ok() { + # Namespaced sandbox works without the setuid helper on kernels that allow + # unprivileged user namespaces. + if [ -r /proc/sys/kernel/unprivileged_userns_clone ]; then + [ "$(cat /proc/sys/kernel/unprivileged_userns_clone)" = "1" ] && return 0 || return 1 + fi + if [ -r /proc/sys/kernel/apparmor_restrict_unprivileged_userns ]; then + [ "$(cat /proc/sys/kernel/apparmor_restrict_unprivileged_userns)" = "0" ] && return 0 || return 1 + fi + [ -e /proc/self/ns/user ] && return 0 || return 1 +} + +if sandbox_ok || userns_ok; then + exec "$ELECTRON" $COMMON_ARGS "$APP_DIR" "$@" +fi + +if [ "${STEPFORGE_ALLOW_NO_SANDBOX:-}" = "1" ] || [ "${ELECTRON_DISABLE_SANDBOX:-}" = "1" ]; then + echo "stepforge: launching WITHOUT the Chromium sandbox (explicit opt-in)." >&2 + exec "$ELECTRON" --no-sandbox $COMMON_ARGS "$APP_DIR" "$@" +fi + +cat >&2 <<'MSG' +stepforge: the Chromium sandbox is not available and StepForge will not launch +unsandboxed by default. + +Fix one of the following: + * Make the setuid sandbox helper usable: + sudo chown root:root /opt/stepforge/node_modules/electron/dist/chrome-sandbox + sudo chmod 4755 /opt/stepforge/node_modules/electron/dist/chrome-sandbox + * Enable unprivileged user namespaces (kernel/sysctl dependent): + sudo sysctl -w kernel.unprivileged_userns_clone=1 + +For development/CI only you may set STEPFORGE_ALLOW_NO_SANDBOX=1 to override. +MSG +exit 1 diff --git a/packaging/linux/common/stepforge-mime.xml b/packaging/linux/common/stepforge-mime.xml new file mode 100644 index 0000000..9c53df5 --- /dev/null +++ b/packaging/linux/common/stepforge-mime.xml @@ -0,0 +1,13 @@ + + + + StepForge guide archive + + + + + StepForge export template + + + + diff --git a/packaging/linux/common/stepforge.desktop b/packaging/linux/common/stepforge.desktop new file mode 100644 index 0000000..2c1565f --- /dev/null +++ b/packaging/linux/common/stepforge.desktop @@ -0,0 +1,13 @@ +[Desktop Entry] +Type=Application +Name=StepForge +GenericName=Step-by-step guide capture +Comment=Capture, annotate, and export step-by-step guides +Exec=stepforge %U +Icon=stepforge +Terminal=false +Categories=Office;Graphics;Utility; +Keywords=documentation;screenshot;guide;capture;steps; +StartupNotify=true +StartupWMClass=StepForge +MimeType=application/x-stepforge-guide; diff --git a/packaging/linux/debian/control.in b/packaging/linux/debian/control.in new file mode 100644 index 0000000..c5e4890 --- /dev/null +++ b/packaging/linux/debian/control.in @@ -0,0 +1,17 @@ +Package: stepforge +Version: @VERSION@ +Section: utils +Priority: optional +Architecture: @ARCH@ +Depends: libnss3, libnspr4, libatk1.0-0, libatk-bridge2.0-0, libcups2, libgbm1, libasound2, libgtk-3-0, libxkbcommon0, libatspi2.0-0 +Recommends: xinput, x11-utils, xdg-desktop-portal, pipewire +Maintainer: @MAINTAINER@ +Homepage: https://github.com/Twest2/StepForge +Description: Local-first step-by-step guide capture and export tool + StepForge captures step-by-step workflows as screenshots, lets you annotate + and describe each step, and exports to Markdown, PDF, DOCX, PPTX, HTML, and + more. Local-first: no telemetry, with an optional user-configured local AI + integration. + . + This package bundles a fixed Electron runtime and only production + dependencies; it does not install anything at runtime. diff --git a/packaging/linux/debian/package.sh b/packaging/linux/debian/package.sh new file mode 100755 index 0000000..59503eb --- /dev/null +++ b/packaging/linux/debian/package.sh @@ -0,0 +1,143 @@ +#!/usr/bin/env bash +# Build a production StepForge .deb (and a matching portable tarball) from a +# pruned, runtime-only tree. +# +# Unlike the old scripts/package-linux.sh this does NOT copy the development +# node_modules, docs, prompts, examples, or stale audit files; it stages only +# the app code plus a runtime dependency set (the fixed Electron runtime and +# production npm deps), a real desktop entry, icons, MIME registration, and a +# license. Architecture is detected, not hardcoded. +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../.." && pwd)" +cd "$ROOT_DIR" + +VERSION="$(node -p "require('./package.json').version")" +MAINTAINER="${STEPFORGE_MAINTAINER:-StepForge }" +OUT_DIR="${STEPFORGE_PACKAGE_DIR:-$ROOT_DIR/build/artifacts}" +mkdir -p "$OUT_DIR" + +# Map dpkg architecture to a Node-style label for the tarball name. +DEB_ARCH="$(dpkg --print-architecture 2>/dev/null || echo amd64)" +case "$DEB_ARCH" in + amd64) NODE_ARCH="x64" ;; + arm64) NODE_ARCH="arm64" ;; + *) NODE_ARCH="$DEB_ARCH" ;; +esac + +# A packaged app must contain a fixed runtime; never install at build time from +# within the package step, and never ship without node_modules. +if [ ! -d "$ROOT_DIR/node_modules/electron/dist" ]; then + echo "error: node_modules/electron is missing. Run 'npm ci' before packaging." >&2 + exit 1 +fi + +WORK_DIR="$(mktemp -d "${OUT_DIR%/}/.deb.XXXXXX")" +trap 'rm -rf "$WORK_DIR"' EXIT +APP_DIR="$WORK_DIR/opt/stepforge" +mkdir -p "$APP_DIR" "$WORK_DIR/usr/bin" "$WORK_DIR/DEBIAN" +mkdir -p "$WORK_DIR/usr/share/applications" +mkdir -p "$WORK_DIR/usr/share/mime/packages" +mkdir -p "$WORK_DIR/usr/share/doc/stepforge" + +# --- application code (runtime only) ---------------------------------------- +for item in app core exporters package.json package-lock.json; do + cp -a "$ROOT_DIR/$item" "$APP_DIR/$item" +done + +# --- runtime node_modules ---------------------------------------------------- +# The fixed Electron runtime (needed at runtime even though it is a dev dep): +mkdir -p "$APP_DIR/node_modules" +cp -a "$ROOT_DIR/node_modules/electron" "$APP_DIR/node_modules/electron" +# Production npm dependencies (tesseract.js + language data + transitive): +while IFS= read -r dep; do + [ -n "$dep" ] || continue + rel="${dep#"$ROOT_DIR"/}" + [ "$rel" != "$dep" ] || continue # only paths under the repo + [ -d "$dep" ] || continue + mkdir -p "$APP_DIR/$(dirname "$rel")" + cp -a "$dep" "$APP_DIR/$rel" +done < <(npm ls --omit=dev --all --parseable 2>/dev/null | tail -n +2) + +# Guard: the development-only packaging toolchain must not have leaked in. +if [ -d "$APP_DIR/node_modules/electron-builder" ] || [ -d "$APP_DIR/node_modules/app-builder-lib" ]; then + echo "error: build-only dependency leaked into the package payload." >&2 + exit 1 +fi + +# --- launcher ---------------------------------------------------------------- +install -m 0755 "$ROOT_DIR/packaging/linux/common/launcher.sh" "$WORK_DIR/usr/bin/stepforge" + +# --- desktop entry, icons, MIME --------------------------------------------- +install -m 0644 "$ROOT_DIR/packaging/linux/common/stepforge.desktop" "$WORK_DIR/usr/share/applications/stepforge.desktop" +install -m 0644 "$ROOT_DIR/packaging/linux/common/stepforge-mime.xml" "$WORK_DIR/usr/share/mime/packages/stepforge.xml" +for size in 16 32 48 64 128 256 512; do + icon="$ROOT_DIR/packaging/assets/icons/stepforge-${size}.png" + [ -f "$icon" ] || continue + dest="$WORK_DIR/usr/share/icons/hicolor/${size}x${size}/apps" + mkdir -p "$dest" + install -m 0644 "$icon" "$dest/stepforge.png" +done + +# --- license + docs pointer -------------------------------------------------- +if [ -f "$ROOT_DIR/LICENSE" ]; then + install -m 0644 "$ROOT_DIR/LICENSE" "$WORK_DIR/usr/share/doc/stepforge/copyright" +elif [ -f "$ROOT_DIR/docs/LICENSE" ]; then + install -m 0644 "$ROOT_DIR/docs/LICENSE" "$WORK_DIR/usr/share/doc/stepforge/copyright" +fi + +# --- DEBIAN control + maintainer scripts ------------------------------------ +sed -e "s/@VERSION@/$VERSION/" -e "s/@ARCH@/$DEB_ARCH/" -e "s#@MAINTAINER@#$MAINTAINER#" \ + "$ROOT_DIR/packaging/linux/debian/control.in" > "$WORK_DIR/DEBIAN/control" + +cat > "$WORK_DIR/DEBIAN/postinst" <<'POSTINST' +#!/bin/sh +set -e +# Make the Chromium setuid sandbox helper usable so the app launches sandboxed. +HELPER=/opt/stepforge/node_modules/electron/dist/chrome-sandbox +if [ -e "$HELPER" ]; then + chown root:root "$HELPER" || true + chmod 4755 "$HELPER" || true +fi +# Refresh desktop/MIME/icon caches (best effort). +if command -v update-desktop-database >/dev/null 2>&1; then update-desktop-database -q /usr/share/applications || true; fi +if command -v update-mime-database >/dev/null 2>&1; then update-mime-database /usr/share/mime || true; fi +if command -v gtk-update-icon-cache >/dev/null 2>&1; then gtk-update-icon-cache -q /usr/share/icons/hicolor || true; fi +exit 0 +POSTINST + +cat > "$WORK_DIR/DEBIAN/prerm" <<'PRERM' +#!/bin/sh +set -e +exit 0 +PRERM + +cat > "$WORK_DIR/DEBIAN/postrm" <<'POSTRM' +#!/bin/sh +set -e +if [ "$1" = "remove" ] || [ "$1" = "purge" ]; then + if command -v update-desktop-database >/dev/null 2>&1; then update-desktop-database -q /usr/share/applications || true; fi + if command -v update-mime-database >/dev/null 2>&1; then update-mime-database /usr/share/mime || true; fi + if command -v gtk-update-icon-cache >/dev/null 2>&1; then gtk-update-icon-cache -q /usr/share/icons/hicolor || true; fi +fi +exit 0 +POSTRM +chmod 0755 "$WORK_DIR/DEBIAN/postinst" "$WORK_DIR/DEBIAN/prerm" "$WORK_DIR/DEBIAN/postrm" + +# --- build the .deb ---------------------------------------------------------- +DEB_FILE="$OUT_DIR/stepforge_${VERSION}_${DEB_ARCH}.deb" +if command -v fakeroot >/dev/null 2>&1; then + fakeroot dpkg-deb --build "$WORK_DIR" "$DEB_FILE" >/dev/null +else + dpkg-deb --build "$WORK_DIR" "$DEB_FILE" >/dev/null +fi + +# --- portable tarball (INCLUDES the launcher, unlike the old script) --------- +TAR_FILE="$OUT_DIR/stepforge_${VERSION}_linux-${NODE_ARCH}.tar.gz" +tar -C "$WORK_DIR" -czf "$TAR_FILE" opt usr/bin/stepforge usr/share/applications usr/share/mime usr/share/icons + +# --- checksums --------------------------------------------------------------- +( cd "$OUT_DIR" && sha256sum "$(basename "$DEB_FILE")" "$(basename "$TAR_FILE")" > "stepforge_${VERSION}_${DEB_ARCH}.sha256" ) + +echo "$DEB_FILE" +echo "$TAR_FILE" diff --git a/scripts/build-release.sh b/scripts/build-release.sh index e5c51d3..0d13067 100644 --- a/scripts/build-release.sh +++ b/scripts/build-release.sh @@ -14,7 +14,14 @@ mkdir -p "$BUILD_ROOT" bash "$ROOT_DIR/scripts/bootstrap-offline.sh" node "$ROOT_DIR/scripts/make-sample-guide.js" --root "$EXAMPLES_ROOT" -STEPFORGE_PACKAGE_DIR="$ARTIFACT_DIR" bash "$ROOT_DIR/scripts/package-linux.sh" >/dev/null +# Production Linux package: a pruned runtime tree with real desktop +# integration. Requires node_modules (fails otherwise); never installs at +# build time. Skipped only when the Electron runtime is genuinely absent. +if [ -d "$ROOT_DIR/node_modules/electron/dist" ]; then + STEPFORGE_PACKAGE_DIR="$ARTIFACT_DIR" bash "$ROOT_DIR/packaging/linux/debian/package.sh" >/dev/null +else + echo "[build-release] skipping Linux .deb: node_modules/electron missing (run npm ci)" >&2 +fi BUILD_ROOT="$BUILD_ROOT" \ ARTIFACT_DIR="$ARTIFACT_DIR" \ diff --git a/scripts/linux/apt/install-build-deps.sh b/scripts/linux/apt/install-build-deps.sh new file mode 100755 index 0000000..e740bbf --- /dev/null +++ b/scripts/linux/apt/install-build-deps.sh @@ -0,0 +1,32 @@ +#!/usr/bin/env bash +# Install the BUILD toolchain for producing StepForge packages on apt-based +# systems. These are for DEVELOPERS/packagers only and are never shipped inside +# the end-user package. +set -euo pipefail + +if ! command -v apt-get >/dev/null 2>&1; then + echo "This script is for apt-based systems (Debian/Ubuntu)." >&2 + exit 1 +fi + +SUDO="" +if [ "$(id -u)" -ne 0 ]; then SUDO="sudo"; fi + +PACKAGES=( + dpkg-dev fakeroot # build the .deb + desktop-file-utils # validate the .desktop entry + ca-certificates # npm ci over https + xvfb # headless smoke test under Xvfb +) + +echo "Installing StepForge build dependencies via apt..." +$SUDO apt-get update +$SUDO apt-get install -y --no-install-recommends "${PACKAGES[@]}" + +cat <<'MSG' +Done. Also install the pinned Node toolchain (see .nvmrc — Node 22.12+): + nvm install && nvm use # or another Node 22 LTS install method +Then, from the repo root: + npm ci + npm run package:linux:deb +MSG diff --git a/scripts/linux/apt/install-runtime-deps.sh b/scripts/linux/apt/install-runtime-deps.sh new file mode 100755 index 0000000..711b1d8 --- /dev/null +++ b/scripts/linux/apt/install-runtime-deps.sh @@ -0,0 +1,31 @@ +#!/usr/bin/env bash +# Install the RUNTIME libraries StepForge needs on apt-based systems +# (Debian/Ubuntu). These are the shared libraries the packaged Electron runtime +# links against, plus the X11/portal integration used for capture. This is for +# END USERS installing from the tarball; the .deb declares the same set as +# Depends so apt pulls them automatically. +set -euo pipefail + +if ! command -v apt-get >/dev/null 2>&1; then + echo "This script is for apt-based systems (Debian/Ubuntu). Use the dnf script on Fedora." >&2 + exit 1 +fi + +SUDO="" +if [ "$(id -u)" -ne 0 ]; then SUDO="sudo"; fi + +PACKAGES=( + # Chromium/Electron shared libraries + libnss3 libnspr4 libatk1.0-0 libatk-bridge2.0-0 libcups2 libdrm2 + libgtk-3-0 libgbm1 libasound2 libxkbcommon0 libatspi2.0-0 + libxcomposite1 libxdamage1 libxfixes3 libxrandr2 libxshmfence1 + # X11 per-click capture (marker-accurate) — X11 sessions only + xinput x11-utils + # Wayland screen-share via the XDG portal + PipeWire + xdg-desktop-portal pipewire +) + +echo "Installing StepForge runtime dependencies via apt..." +$SUDO apt-get update +$SUDO apt-get install -y --no-install-recommends "${PACKAGES[@]}" +echo "Done. StepForge runtime dependencies are installed." diff --git a/scripts/make-icons.js b/scripts/make-icons.js new file mode 100644 index 0000000..c5e6a5d --- /dev/null +++ b/scripts/make-icons.js @@ -0,0 +1,69 @@ +#!/usr/bin/env node +'use strict'; + +// Generate the StepForge PNG icon set from original geometry using the repo's +// own rasterizer + PNG writer (no external image tooling or third-party art). +// Mirrors packaging/assets/stepforge.svg. Output: packaging/assets/icons/. + +const fs = require('node:fs'); +const path = require('node:path'); +const { createImage, fillRect, fillOval } = require('../core/raster'); +const { encodePng } = require('../core/png'); + +const OUT_DIR = path.join(__dirname, '..', 'packaging', 'assets', 'icons'); +const SIZES = [16, 32, 48, 64, 128, 256, 512]; + +const BG_TOP = [37, 99, 235, 255]; +const BG_BOTTOM = [30, 58, 138, 255]; +const WHITE = [255, 255, 255, 255]; +const SPARK = [250, 204, 21, 255]; + +function lerp(a, b, t) { + return [ + Math.round(a[0] + (b[0] - a[0]) * t), + Math.round(a[1] + (b[1] - a[1]) * t), + Math.round(a[2] + (b[2] - a[2]) * t), + 255, + ]; +} + +function renderIcon(size) { + const img = createImage(size, size, [0, 0, 0, 0]); + const s = size / 256; // scale from the 256px reference design + + // Rounded-square background approximated by a vertical gradient fill. + for (let y = 0; y < size; y += 1) { + fillRect(img, 0, y, size, 1, lerp(BG_TOP, BG_BOTTOM, y / size)); + } + + // Three ascending steps (x, y, w, h in reference px). + const steps = [ + [52, 150, 52, 54], + [102, 116, 52, 88], + [152, 82, 52, 122], + ]; + for (const [x, y, w, h] of steps) { + fillRect(img, Math.round(x * s), Math.round(y * s), Math.round(w * s), Math.round(h * s), WHITE); + } + + // Capture spark on the top step. + const r = Math.max(2, Math.round(16 * s)); + fillOval(img, Math.round(178 * s - r), Math.round(60 * s - r), r * 2, r * 2, SPARK); + + return img; +} + +function main() { + fs.mkdirSync(OUT_DIR, { recursive: true }); + for (const size of SIZES) { + const png = encodePng(renderIcon(size)); + fs.writeFileSync(path.join(OUT_DIR, `stepforge-${size}.png`), png); + } + // A conventional default name for the desktop entry / hicolor 256px slot. + fs.copyFileSync(path.join(OUT_DIR, 'stepforge-256.png'), path.join(OUT_DIR, 'stepforge.png')); + console.log(`wrote ${SIZES.length + 1} icons to ${OUT_DIR}`); +} + +if (require.main === module) main(); + +module.exports = { renderIcon, SIZES }; diff --git a/scripts/package-linux.sh b/scripts/package-linux.sh deleted file mode 100644 index cf0fc0d..0000000 --- a/scripts/package-linux.sh +++ /dev/null @@ -1,92 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" -VERSION="$(node -p "const pkg=require('${ROOT_DIR}/package.json'); pkg.buildVersion || pkg.version" 2>/dev/null || echo 0.0.0)" -OUT_DIR="${STEPFORGE_PACKAGE_DIR:-$ROOT_DIR/build/artifacts}" -mkdir -p "$OUT_DIR" -WORK_DIR="$(mktemp -d "${OUT_DIR%/}/.pkg.XXXXXX")" -APP_DIR="$WORK_DIR/opt/stepforge" - -cleanup() { - rm -rf "$WORK_DIR" -} -trap cleanup EXIT - -mkdir -p "$APP_DIR" "$WORK_DIR/usr/bin" "$WORK_DIR/DEBIAN" - -copy_item() { - local src="$1" - local dest="$2" - if [[ -e "$ROOT_DIR/$src" ]]; then - mkdir -p "$(dirname "$dest")" - cp -a "$ROOT_DIR/$src" "$dest" - fi -} - -# Application payload: only the files needed to run the app. -copy_item app "$APP_DIR/app" -copy_item core "$APP_DIR/core" -copy_item exporters "$APP_DIR/exporters" -copy_item scripts "$APP_DIR/scripts" -copy_item README.md "$APP_DIR/README.md" -copy_item LICENSE "$APP_DIR/LICENSE" -copy_item docs "$APP_DIR/docs" -copy_item ai_prompts "$APP_DIR/ai_prompts" -copy_item package.json "$APP_DIR/package.json" -copy_item package-lock.json "$APP_DIR/package-lock.json" -copy_item examples "$APP_DIR/examples" -copy_item build/agent_audit.md "$APP_DIR/build/agent_audit.md" - -if [[ -d "$ROOT_DIR/node_modules" ]]; then - cp -a "$ROOT_DIR/node_modules" "$APP_DIR/node_modules" -fi - -cat > "$WORK_DIR/usr/bin/stepforge" <<'EOF' -#!/usr/bin/env sh -APP_DIR=/opt/stepforge -ELECTRON="$APP_DIR/node_modules/.bin/electron" -SANDBOX_HELPER="$APP_DIR/node_modules/electron/dist/chrome-sandbox" -cd "$APP_DIR" || exit 1 -if command -v stat >/dev/null 2>&1 && [ -e "$SANDBOX_HELPER" ]; then - helper_uid="$(stat -c '%u' "$SANDBOX_HELPER" 2>/dev/null || echo '')" - helper_mode="$(stat -c '%a' "$SANDBOX_HELPER" 2>/dev/null || echo '')" - if [ "$helper_uid" = "0" ] && [ -n "$helper_mode" ]; then - helper_mode_num=$((8#$helper_mode)) - if [ $((helper_mode_num & 04000)) -ne 0 ]; then - exec "$ELECTRON" "$APP_DIR" "$@" - fi - fi -fi -printf '%s\n' '[stepforge] Electron sandbox helper is not configured for this install; starting with --no-sandbox' >&2 -exec "$ELECTRON" --no-sandbox "$APP_DIR" "$@" -EOF -chmod 0755 "$WORK_DIR/usr/bin/stepforge" - -cat > "$WORK_DIR/DEBIAN/control" < -Description: Offline desktop guide capture and export tool - A fully offline desktop app for step-by-step documentation, built for local - capture, annotation, and export workflows. -EOF - -DEB_FILE="$OUT_DIR/stepforge_${VERSION}_amd64.deb" -TAR_FILE="$OUT_DIR/stepforge_${VERSION}_linux-x64.tar.gz" - -if command -v dpkg-deb >/dev/null 2>&1; then - dpkg-deb --build "$WORK_DIR" "$DEB_FILE" >/dev/null -else - echo "dpkg-deb is not installed; skipping .deb build" >&2 -fi - -tar -C "$WORK_DIR/opt" -czf "$TAR_FILE" stepforge - -printf '%s\n' "$DEB_FILE" -printf '%s\n' "$TAR_FILE" diff --git a/tests/integration/linux/package-deb.test.sh b/tests/integration/linux/package-deb.test.sh new file mode 100644 index 0000000..af9d057 --- /dev/null +++ b/tests/integration/linux/package-deb.test.sh @@ -0,0 +1,78 @@ +#!/usr/bin/env bash +# Integration test: build the production .deb and assert it is a real, +# runtime-only package — the right files present, and the dev tree / build +# tooling / app docs absent. A package is NOT accepted merely because +# dpkg-deb produced a file. +# +# Honest skip policy: skip ONLY when the prerequisites are genuinely absent +# (not apt-based, dpkg-deb missing, or node_modules not installed). Once we +# build, any structural failure fails the test. +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../.." && pwd)" +cd "$ROOT_DIR" + +if ! command -v dpkg-deb >/dev/null 2>&1; then + echo "package-deb SKIPPED: dpkg-deb not installed (not an apt-based build host)" + exit 0 +fi +if [ ! -d "$ROOT_DIR/node_modules/electron/dist" ]; then + echo "package-deb SKIPPED: node_modules/electron missing (run npm ci first)" + exit 0 +fi + +OUT_DIR="$(mktemp -d)" +trap 'rm -rf "$OUT_DIR"' EXIT + +DEB="$(STEPFORGE_PACKAGE_DIR="$OUT_DIR" bash packaging/linux/debian/package.sh | head -1)" +if [ ! -f "$DEB" ]; then + echo "package-deb FAILED: builder did not produce a .deb" >&2 + exit 1 +fi + +fail() { echo "package-deb FAILED: $1" >&2; exit 1; } + +listing="$(dpkg-deb -c "$DEB")" +control="$(dpkg-deb -f "$DEB")" + +# Required install items. +for needle in \ + './usr/bin/stepforge' \ + './usr/share/applications/stepforge.desktop' \ + './usr/share/mime/packages/stepforge.xml' \ + './usr/share/icons/hicolor/256x256/apps/stepforge.png' \ + './opt/stepforge/node_modules/electron/dist/electron' \ + './opt/stepforge/app/main.js' \ + './usr/share/doc/stepforge/copyright'; do + echo "$listing" | grep -qF "$needle" || fail "missing packaged file: $needle" +done + +# The development node_modules / build tooling must NOT be present. +for banned in \ + 'node_modules/electron-builder' \ + 'node_modules/app-builder-lib' \ + 'node_modules/dmg-builder'; do + echo "$listing" | grep -qF "$banned" && fail "build-only dependency leaked: $banned" || true +done + +# The app's own docs/prompts/examples must not be shipped. +for banned in \ + './opt/stepforge/docs/' \ + './opt/stepforge/ai_prompts/' \ + './opt/stepforge/examples/'; do + echo "$listing" | grep -qF "$banned" && fail "app extra shipped: $banned" || true +done + +# Control metadata sanity. +echo "$control" | grep -q '^Package: stepforge' || fail "control missing Package" +echo "$control" | grep -q '^Depends:.*libnss3' || fail "control missing runtime Depends" +echo "$control" | grep -Eq '^Architecture: (amd64|arm64)' || fail "control has no concrete Architecture" + +# Sandbox is set up, not disabled: postinst makes chrome-sandbox setuid. +dpkg-deb --info "$DEB" | grep -q 'postinst' || fail "no postinst maintainer script" + +# The launcher must refuse an unsandboxed launch by default. +grep -q 'STEPFORGE_ALLOW_NO_SANDBOX' packaging/linux/common/launcher.sh \ + || fail "launcher does not gate --no-sandbox behind an explicit opt-in" + +echo "package-deb OK ($(basename "$DEB"), $(du -h "$DEB" | cut -f1))" diff --git a/tests/unit/packaging-linux.test.js b/tests/unit/packaging-linux.test.js new file mode 100644 index 0000000..ed25940 --- /dev/null +++ b/tests/unit/packaging-linux.test.js @@ -0,0 +1,97 @@ +'use strict'; + +const test = require('node:test'); +const assert = require('node:assert/strict'); +const fs = require('node:fs'); +const path = require('node:path'); + +const ROOT = path.resolve(__dirname, '..', '..'); +const read = (rel) => fs.readFileSync(path.join(ROOT, rel), 'utf8'); +const exists = (rel) => fs.existsSync(path.join(ROOT, rel)); + +// These are structural checks that run in the normal (cross-platform) unit +// suite so the Linux packaging config can't rot silently; the actual .deb +// build/inspection lives in tests/integration/linux/package-deb.test.sh. + +test('Linux packaging files exist in their expected separate locations', () => { + for (const f of [ + 'packaging/linux/debian/package.sh', + 'packaging/linux/debian/control.in', + 'packaging/linux/common/stepforge.desktop', + 'packaging/linux/common/stepforge-mime.xml', + 'packaging/linux/common/launcher.sh', + 'scripts/linux/apt/install-runtime-deps.sh', + 'scripts/linux/apt/install-build-deps.sh', + 'docs/linux/apt.md', + 'tests/integration/linux/package-deb.test.sh', + ]) { + assert.ok(exists(f), `expected ${f} to exist`); + } +}); + +test('the old non-production package-linux.sh is gone', () => { + assert.equal(exists('scripts/package-linux.sh'), false); +}); + +test('the desktop entry is valid and app-branded', () => { + const desktop = read('packaging/linux/common/stepforge.desktop'); + assert.match(desktop, /^\[Desktop Entry\]/); + assert.match(desktop, /^Type=Application$/m); + assert.match(desktop, /^Exec=stepforge %U$/m); + assert.match(desktop, /^Icon=stepforge$/m); + assert.match(desktop, /^Categories=.+;$/m); + assert.match(desktop, /MimeType=application\/x-stepforge-guide/); +}); + +test('the control template declares runtime deps, no hardcoded arch, real maintainer slot', () => { + const control = read('packaging/linux/debian/control.in'); + assert.match(control, /^Architecture: @ARCH@$/m, 'arch must be templated, not hardcoded'); + assert.match(control, /^Depends:.*libnss3/m); + assert.match(control, /@VERSION@/); + assert.match(control, /@MAINTAINER@/); + // The false "fully offline" wording must not reappear here. + assert.doesNotMatch(control, /fully offline/i); +}); + +test('the launcher refuses an unsandboxed launch unless explicitly opted in', () => { + const launcher = read('packaging/linux/common/launcher.sh'); + assert.match(launcher, /STEPFORGE_ALLOW_NO_SANDBOX/); + // It must not unconditionally exec with --no-sandbox. + assert.doesNotMatch(launcher, /^exec .*--no-sandbox/m); + // It never installs anything at runtime. + assert.doesNotMatch(launcher, /npm (install|ci|rebuild)/); +}); + +test('the package builder requires node_modules and guards against dev-dep leaks', () => { + const script = read('packaging/linux/debian/package.sh'); + assert.match(script, /node_modules\/electron\/dist/); + assert.match(script, /npm ls --omit=dev/); + assert.match(script, /electron-builder/); // the leak guard references it + assert.match(script, /dpkg --print-architecture/); // arch detected, not hardcoded + assert.doesNotMatch(script, /cp -a "\$ROOT_DIR\/node_modules" /); // never copy the whole dev tree +}); + +test('apt setup scripts target apt and keep build vs runtime deps separate', () => { + const runtime = read('scripts/linux/apt/install-runtime-deps.sh'); + const build = read('scripts/linux/apt/install-build-deps.sh'); + assert.match(runtime, /apt-get/); + assert.match(runtime, /libnss3/); + assert.doesNotMatch(runtime, /dpkg-dev|fakeroot/, 'runtime script must not install build tools'); + assert.match(build, /dpkg-dev/); + assert.match(build, /fakeroot/); +}); + +test('an original icon set is generated (not a placeholder/third-party asset)', () => { + assert.ok(exists('packaging/assets/stepforge.svg')); + assert.ok(exists('scripts/make-icons.js')); + // The generated PNGs are committed for packaging. + for (const size of [16, 48, 256]) { + assert.ok(exists(`packaging/assets/icons/stepforge-${size}.png`), `icon ${size} missing`); + } + // Regenerate the 256px icon and confirm the generator is deterministic and + // produces a valid PNG (starts with the PNG signature). + const { renderIcon } = require('../../scripts/make-icons'); + const { encodePng } = require('../../core/png'); + const png = encodePng(renderIcon(256)); + assert.deepEqual([...png.subarray(0, 4)], [0x89, 0x50, 0x4e, 0x47]); +});