Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
197 changes: 197 additions & 0 deletions .github/workflows/bundle.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
name: Bundle

# אורז את **החבילה המלאה**: ה-exe שכבר פורסם, ואחריו מראה שלמה של כל
# התוספים משורשרת לסופו. מי שמוריד אותה מריץ פעם אחת, והכול נפרס
# לתיקייה שלידה — בלי אינטרנט, אף פעם.
#
# ⚠️ ה-workflow הזה **אינו מקדם גרסה ואינו מתייג `v<N>`**. הגרסה היא
# מספר שלם אחד שמתקדם רק בדחיפה ל-main (ci.yml), ו-release בשם כזה כאן
# היה מודיע לכל המשתמשים על גרסה חדשה כשהקוד זהה — ראו
# `store_release_client.js`, שבוחר את ה-`v<N>` היציב הגבוה כ"יש עדכון".
#
# לכן התג הוא `bundle` **מתגלגל**: תמיד אותו אחד, מתעדכן במקום. גם
# משום ש-`fetchLatestStable` קורא דף אחד של 30 releases — פרסום חדש בכל
# ריצה היה דוחף אחרי כמה חודשים את כל ה-`v<N>` מחוץ לדף, והתראת העדכון
# הייתה מפסיקה לעבוד בשקט.

on:
# יומי ולא שבועי: הזיהוי זול (הזריעה + מטא-דאטה), והאריזה קורית רק
# כשמשהו באמת זז. עדכון תוסף לא ימתין שבוע.
schedule:
- cron: "0 3 * * *"
# release חדש של החנות עצמה — ה-exe שבתוך החבילה התיישן. אירוע, לא סקר.
workflow_run:
workflows: ["CI"]
types: [completed]
branches: [main]
workflow_dispatch:

permissions:
contents: write

# אריזה אחת בכל רגע: שתי ריצות במקביל היו נלחמות על אותו תג.
concurrency:
group: bundle
cancel-in-progress: false

jobs:
bundle:
name: pack the full bundle
# ⚠️ **windows דווקא**: `mirror_store.js` בונה נתיבים מוחלטים עם `\`,
# ועל לינוקס זה מייצר קבצים ששם הקובץ שלהם מכיל backslash.
runs-on: windows-latest
# דחייה של ריצת CI שנכשלה — אין טעם לארוז exe שלא פורסם.
if: >-
github.event_name != 'workflow_run' ||
github.event.workflow_run.conclusion == 'success'
env:
GH_TOKEN: ${{ github.token }}
BUNDLE_TAG: bundle
BUNDLE_ASSET: Otzaria-Plugin-Store-Full.exe
SLIM_ASSET: Otzaria-Plugin-Store.exe

steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "22"

# ── ה-exe שייכנס לחבילה ────────────────────────────────────────────
# נמשך מה-release ולא נבנה כאן: מה שמופץ בחבילה חייב להיות בדיוק
# הבינארי שנבדק ופורסם, ולא בנייה שנייה של אותו קומיט.
- name: Fetch the published exe
shell: bash
run: |
set -euo pipefail
# הגבוה מבין `v<N>`, כמו `storeVersionOf` בתוכנה — סדר הפרסום
# אינו סדר הגרסאות, ו"הראשון ברשימה" היה תלוי-מזל.
tag="$(gh release list --limit 100 --json tagName \
--jq '[.[].tagName | select(test("^v[0-9]+$"))]
| sort_by(.[1:] | tonumber) | last')"
if [ -z "$tag" ] || [ "$tag" = "null" ]; then
echo "::error::אין אף release בצורה v<N> — אין exe לארוז"
exit 1
fi
echo "ה-exe נלקח מ-$tag"
echo "STORE_TAG=$tag" >> "$GITHUB_ENV"
gh release download "$tag" -p "$SLIM_ASSET" -D slim

# ── זריעה מהחבילה הקודמת ───────────────────────────────────────────
# בלעדיה כל ריצה הייתה מושכת ‎96MB מ-otzaria.org. איתה `#plan`
# מוריד רק את הדלתא — ובדרך אגב זה מריץ ב-CI בדיוק את המסלול
# האינקרמנטלי שכונן של משתמש עובר בעדכון.
- name: Seed from the previous bundle
shell: bash
run: |
set -euo pipefail
if gh release download "$BUNDLE_TAG" -p "$BUNDLE_ASSET" -D previous
then
node native/tools/unpack_overlay.mjs "previous/$BUNDLE_ASSET" Data
cp Data/catalog.json catalog.before.json
node native/tools/mirror_changed.mjs --inventory Data/plugins \
> inventory.before.txt
# ה-exe שבתוך החבילה הקודמת — להשוואה מול זה שהורד עכשיו.
node native/tools/unpack_overlay.mjs --slim \
"previous/$BUNDLE_ASSET" previous-slim.exe
else
echo "אין חבילה קודמת — הפרסום הראשון בונה מראה מאפס"
fi

# ── הסנכרון ────────────────────────────────────────────────────────
# מריץ את `PluginMirrorSync` **עצמו**. כללי הבחירה — איזה בילד לאיזו
# גרסת אוצריא, מה נחשב "לא השתנה" — נשארים במקום אחד, ואין כאן
# מימוש שני שלהם שייפרד מהקוד ביום שאחד מהם ישתנה.
- name: Build the mirror
shell: bash
run: node native/tools/pack_mirror.mjs Data

- name: Is there anything to publish?
id: changed
shell: bash
run: |
# ⚠️ שני תנאים, ולא רק המראה: כשמתפרסם exe חדש המראה אינה
# משתנה, והשוואת המראה לבדה הייתה משאירה את החבילה עם בינארי
# ישן לנצח — כלומר הטריגר `workflow_run` לא היה עושה כלום.
if [ ! -f previous-slim.exe ] ||
! cmp -s "slim/$SLIM_ASSET" previous-slim.exe; then
echo "ה-exe של החנות השתנה — מפרסמים"
echo "publish=true" >> "$GITHUB_OUTPUT"
exit 0
fi

set +e
node native/tools/mirror_changed.mjs \
catalog.before.json Data/catalog.json \
inventory.before.txt Data/plugins
case $? in
0) echo "publish=true" >> "$GITHUB_OUTPUT" ;;
1) echo "publish=false" >> "$GITHUB_OUTPUT" ;;
*) exit 1 ;;
esac

# ── האריזה ─────────────────────────────────────────────────────────
- name: Pack the bundle
if: steps.changed.outputs.publish == 'true'
shell: bash
run: |
set -euo pipefail
node native/tools/pack_overlay.mjs \
"slim/$SLIM_ASSET" Data "$BUNDLE_ASSET"

# המשתמש של החבילה **מנותק**: חבילה פגומה אינה ניתנת לתיקון אצלו,
# והוא גם לא ידע מה נשבר. הפריסה חוזרת כאן ומושווית לפני שמפרסמים.
- name: Verify the bundle round-trips
if: steps.changed.outputs.publish == 'true'
shell: bash
run: |
set -euo pipefail
node native/tools/unpack_overlay.mjs "$BUNDLE_ASSET" verify
# `diff -r` ולא השוואת שמות: שם כמו `image.webp` חוזר 35 פעם
# בתיקיות שונות, והשוואה לפי שם וגודל בלבד לא הייתה מבחינה
# ביניהם. כאן נבדק **התוכן**, קובץ מול קובץ.
diff -r Data verify
size=$(node -e "
console.log((require('fs').statSync(process.argv[1]).size /
1048576).toFixed(1));
" "$BUNDLE_ASSET")
echo "$BUNDLE_ASSET — ${size}MB"

# ── הפרסום ─────────────────────────────────────────────────────────
- name: Publish to the rolling release
if: steps.changed.outputs.publish == 'true'
shell: bash
run: |
set -euo pipefail
plugins="$(node -e "
const c = require('./Data/catalog.json');
const withFile = c.plugins.filter(
(p) => Object.keys(p.localFiles ?? {}).length > 0).length;
console.log(withFile + '|' + c.targetAppVersions.join(', '));
")"
count="${plugins%%|*}"
targets="${plugins##*|}"
# ⚠️ בלי הזחה: הטקסט נמסר כמו שהוא, ושורה מוזחת ב-markdown של
# GitHub נעשית בלוק קוד.
notes="$(printf '%s\n' \
"חבילה מלאה: $count תוספים ארוזים בתוך קובץ ההרצה." \
"" \
"נבנתה עבור אוצריא $targets, על גבי $STORE_TAG." \
"עודכנה: $(date -u '+%Y-%m-%d')" \
"" \
"הרצה אחת פורסת את הכול לתיקייה \`Data\` שלצד קובץ ההרצה, ומשם התוכנה עובדת בלי אינטרנט.")"

# ⚠️ `--clobber` ותג קבוע: החבילה **מתגלגלת**. release חדש בכל
# ריצה היה צובר עשרות פריטים ודוחף את ה-`v<N>` מחוץ לדף שבדיקת
# העדכון קוראת.
if gh release view "$BUNDLE_TAG" >/dev/null 2>&1; then
gh release edit "$BUNDLE_TAG" --notes "$notes"
gh release upload "$BUNDLE_TAG" "$BUNDLE_ASSET" --clobber
else
gh release create "$BUNDLE_TAG" "$BUNDLE_ASSET" \
--title "חבילה מלאה" --notes "$notes"
fi

- name: Nothing changed
if: steps.changed.outputs.publish != 'true'
shell: bash
run: echo "המראה זהה לזו שכבר פורסמה — לא פורסמה חבילה חדשה"
86 changes: 84 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ Data\ ← נוצרת לידו בהרצה הראשונה
ב-`Data\`, והתקנה אינה נוגעת ברשת. זה מה שמאפשר להעתיק את התיקייה לכונן
נייד ולהתקין ממנה במחשב מנותק.

ולמי שאין לו רשת גם בפעם הראשונה מתפרסמת [חבילה מלאה](#החבילה-המלאה) —
אותו exe עם כל התוספים כבר בתוכו.

## מה מיוחד כאן

**‎0.5MB, קובץ אחד.** אין מתקין, אין DLL לצד ה-exe, ואין תיקיית חילוץ.
Expand All @@ -35,14 +38,51 @@ Data\ ← נוצרת לידו בהרצה הראשונה
מנוע רינדור שלם (`flutter_windows.dll` לבדו ‎21MB לא-דחוס), ופרסה ‎29MB
לתיקיית `app-files\` בהרצה הראשונה כדי להריץ משם קובץ הרצה שני.

## החבילה המלאה

מי שמתקין במחשב מנותק צריך שהתוספים כבר יהיו אצלו. לכן מתפרסמת, לצד
ה-exe הרזה, **חבילה מלאה**: אותו קובץ הרצה בדיוק, עם מראה שלמה של כל
התוספים משורשרת לסופו.

```
Otzaria-Plugin-Store-Full.exe ~96MB, הרצה אחת
חנות התוספים.exe 0.52MB — עותק של עצמו בלי המטען
Data\ כל התוספים, מוכנים
```

הרצה ראשונה פורסת את `Data\`, כותבת עותק רזה של עצמה, מפעילה אותו
ויוצאת. הרזה שואל אם למחוק את קובץ ההתקנה — **ולא מוחק מעצמו**: אותה
חבילה עשויה להיות מיועדת גם למחשב שני, ומחיקה שקטה של ‎96MB שהורדו הייתה
מאלצת הורדה מחדש.

**החילוץ מחליף את `plugins\` ואת `catalog.json` במלואם**, ולא ממזג.
המראה היא נגזרת של (קטלוג, גרסאות היעד) ולא נתוני משתמש — זה בדיוק מה
ש-`pruneUnusedFiles` כבר עושה, רק בבת אחת. `state.json` ו-`logs\`, שהם
מקומיים למכונה, אינם נגעים.

⚠️ **החותמת נכתבת אחרונה**, אחרי שהקובץ האחרון נחת. חילוץ שנקטע משאיר
תיקייה בלי חותמת, וההרצה הבאה עושה אותו שוב — המטען עדיין בתוך ה-exe.
זו כל האטומיות שנדרשת, ולכן אין תיקייה זמנית ואין כפל מקום.

### למה שרשור ולא resource

המטען אינו נדחס: `.otzplugin` הוא ZIP והתמונות webp/png, ו-LZMS עליהם
מחזיר אחוזים בודדים תמורת פרישה של ‎96MB לזיכרון. שרשור לסוף ה-PE משאיר
את הבינארי שפורסם **בייט-לבייט** — כך שהעותק הרזה שנכתב בחילוץ הוא בדיוק
ה-exe שנבדק — ומאפשר חילוץ בזרימה בלי הקצאה גדולה אחת. RCDATA היה מחייב
בנייה מחדש של ה-exe סביב המטען, כלומר בינארי אחר מזה שפורסם.

הפורמט מוגדר ב-`native/tools/pack_overlay.mjs` ונקרא ב-`native/src/overlay.cpp`.

## מבנה

```
native/
src/ ה-host ב-C++: חלון, WebView2, וגשר לקבצים/רשת/רג'יסטרי
web/ הממשק וכל הלוגיקה — HTML/CSS/JS, בלי bundler ובלי npm
test/ בדיקות `node --test`
tools/ כלי בנייה ופיתוח
tools/ כלי בנייה ופיתוח, וכלי האריזה של החבילה המלאה
third_party/ ה-SDK של WebView2 (מובא מ-nuget, אינו בגיט)
build.ps1 הבנייה כולה
VERSION מספר הגרסה — מקום אחד
Expand Down Expand Up @@ -108,6 +148,14 @@ cd native && node --test
node native/tools/seed_catalog.mjs native/build/Data
```

```bash
# בונה מראה מלאה בהרצת הסנכרון האמיתי, ואורז אותה לחבילה — בדיוק מה
# שהג'וב עושה, כדי שאפשר יהיה לשחזר תקלה בלי GitHub.
node native/tools/pack_mirror.mjs Data
node native/tools/pack_overlay.mjs "native/build/חנות התוספים.exe" Data Full.exe
node native/tools/unpack_overlay.mjs Full.exe verify # ואז diff -r Data verify
```

```bash
# מחלץ מחדש את האייקונים מגופני Fluent UI אל native/web/js/ui/icons.js.
# להריץ רק כשמוסיפים אייקון (ראו ICONS שבסקריפט).
Expand Down Expand Up @@ -140,6 +188,37 @@ node native/tools/extract_icons.mjs
התוכנה בודקת פעם אחת בהרצה אם יצא release חדש ומציגה על כך שורה. **אין
עדכון עצמי**: הסיפור מסתיים בכתובת שנפתחת בדפדפן.

### החבילה המלאה — `.github/workflows/bundle.yml`

רץ יומית, וגם כשריצת CI הצליחה ב-`main` (‏exe חדש), וגם ביד. **אינו
מקדם גרסה ואינו מתייג `v<N>`**: התג הוא `bundle` **מתגלגל**, תמיד אותו
אחד, מתעדכן במקום.

שתי סיבות, שתיהן ב-`store_release_client.js`:

* `isStoreReleaseTag` פוסל כל תג שאינו `^v?\d+$`, ולכן `bundle` נעלם
מבדיקת העדכון — אחרת כל אריזה הייתה מודיעה למשתמשים על "גרסה חדשה"
כשהקוד זהה.
* `fetchLatestStable` קורא **דף אחד של 30 releases**. פרסום חדש בכל
ריצה היה דוחף אחרי כמה חודשים את כל ה-`v<N>` מחוץ לדף, והתראת העדכון
הייתה מפסיקה לעבוד בשקט.

הג'וב **זורע את עצמו מהחבילה הקודמת** לפני שהוא מסנכרן, וכך `#plan`
מוריד מ-otzaria.org רק את הדלתא: עדכון של תוסף אחד עולה קובץ אחד ולא
‎96MB. בדרך אגב זה מריץ ב-CI בדיוק את המסלול האינקרמנטלי שכונן של משתמש
עובר בעדכון, ולא "מראה ריקה" שאיש אינו חי בה.

**מה נחשב שינוי** נקבע ב-`mirror_changed.mjs`, בהשוואת התוצאה ולא
בגיבוב של ה-API. ⚠️ המונים (`downloadCount`, הדירוגים) מנוטרלים
מההשוואה: נמדד שסנכרון שדילג על כל 38 התוספים ולא הוריד דבר עדיין
הפיק קטלוג שונה, וכל ההבדל היה מונה ההורדות של תוסף אחד — כלומר בלי
הנטרול הג'וב היה מפרסם ‎96MB בכל ריצה.

⚠️ **לא רק המראה נבדקת אלא גם ה-exe עצמו**: כשמתפרסם בינארי חדש המראה
אינה משתנה, והשוואת המראה לבדה הייתה משאירה את החבילה עם exe ישן לנצח.
הג'וב מחלץ את ה-exe מהחבילה הקודמת (`unpack_overlay.mjs --slim`) ומשווה
בייטים.

## הערות

* **תיקיית הנתונים תמיד צמודה לקובץ ההרצה ואינה ניתנת לשינוי** — זו
Expand All @@ -153,7 +232,10 @@ node native/tools/extract_icons.mjs
ששורדים את סגירת החלון ומחזיקים מנעולים על הקבצים.
* **שם קובץ ההרצה אינו מכיל "אוצריא"** בכוונה: זיהוי ההתקנה של אוצריא
עובד לפי השם, ו-exe בשם כזה שיושב ב-`C:\אוצריא` היה נבחר שם כהתקנה של
אוצריא עצמה.
אוצריא עצמה. ⚠️ הזיהוי מתאים גם ל-`otzaria` הלטיני, ולכן גם
`Otzaria-Plugin-Store-Full.exe` — קובץ ההתקנה של החבילה, שהמשתמש עשוי
לבחור להשאיר — נמצא ב-`OUR_OWN_EXE_NAMES` שב-`otzaria_probe.js`.
‏`selfPath` אינו מכסה אותו: אחרי הפריסה מי שרץ הוא ה-exe הרזה.
* **ההתקנה נמסרת לאוצריא** דרך `otzaria://plugin/install-local` — ולא
בפרישת ה-ZIP בעצמנו, כי אוצריא מנהלת רישום פנימי לתוספים המותקנים
ופרישה ידנית עוקפת אותו. כשנתיב ההתקנה ידוע ה-URL נמסר לקובץ ההרצה
Expand Down
9 changes: 6 additions & 3 deletions native/build.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -95,8 +95,8 @@ if (-not (Test-Path $vcvars)) { throw "vcvars64.bat not found at $vcvars." }

# ── 5. קומפילציה ולינק ───────────────────────────────────────────────────────
$sources = @(
'main.cpp', 'bridge.cpp', 'common.cpp', 'fsapi.cpp',
'netapi.cpp', 'paths.cpp', 'sysapi.cpp', 'webres.cpp'
'main.cpp', 'bridge.cpp', 'common.cpp', 'fsapi.cpp', 'netapi.cpp',
'overlay.cpp', 'paths.cpp', 'sysapi.cpp', 'webres.cpp'
) | ForEach-Object { '"' + (Join-Path $srcDir $_) + '"' }

# /MT — CRT סטטי, וזו החלטה מרכזית: בלעדיו ה-exe דורש
Expand All @@ -118,10 +118,13 @@ if ($Debug) {
$linkExtra = '/OPT:REF /OPT:ICF'
}

# comctl32 — ה-progress bar של חלון פריסת החבילה (overlay.cpp). הוא
# משאיר את ה-exe בגודלו: ה-DLL כבר טעון בכל תהליך שיש לו חלון.
$libs = @(
'version.lib', 'shlwapi.lib', 'dwmapi.lib', 'winhttp.lib', 'cabinet.lib',
'crypt32.lib', 'ole32.lib', 'oleaut32.lib', 'shell32.lib', 'user32.lib',
'gdi32.lib', 'advapi32.lib', 'kernel32.lib', 'comdlg32.lib', 'uuid.lib'
'gdi32.lib', 'advapi32.lib', 'kernel32.lib', 'comdlg32.lib', 'uuid.lib',
'comctl32.lib'
) -join ' '

$exePath = Join-Path $outDir $appFileName
Expand Down
Loading
Loading