הפצת יישום Windows בקובץ אחד - הגבול בין בינארי יחיד לתלות במערכת ההפעלה

· עודכן בתאריך: · · Windows, הפצה, בינארי יחיד, .NET, C++, WebView2, WinUI

המאמר הזה התחיל משיחת חולין - ‘עד כמה מותר לקרוא ל-Windows בינארי יחיד (single binary)?’ הפוסט שהיה הטריגר הוא זה שלמטה. גם אם מדלגים עליו, הכתיבה שאחריו נועדה להיקרא באופן עצמאי.

ב-Windows, הרצון ‘אם אפשר, להפיץ בקובץ אחד’ הוא בקשה די רגילה. בכלים פנים-ארגוניים, כלי חיבור לציוד, מסופי ניטור, סביבות offline, ובשטח שרוצה להימנע ממתקין (installer) ככל האפשר, ל-איחוד לבינארי יחיד יש קסם רב.

עם זאת, אם לא מפרידים את הנושא הזה מראש, בדרך כלל השיחה מפסיקה להתיישר באמצע. הסיבה היא שברצון ‘(רוצים) בינארי יחיד’ כפי שאומרים אותו ב-Windows, נוטים להתערבב בפועל 4 נושאים נפרדים.

  • רוצים לרכז את פריט ההפצה לאחד
  • רוצים לבטל את הצורך בהתקנה מראש של ה-runtime של‏ .NET או Visual C++
  • רוצים שיפעל רק בהנחת קובץ, בלי מתקין ובלי הרשאות מנהל
  • לא רוצים תלות בהבדלים בין גרסאות Windows של היעד

ארבעת אלה אינם זהים. מבחינה מעשית, החשיבה הבאה הכי פחות סוטה:

אפשר לרכז את פריט ההפצה ל-EXE אחד, במידה רבה. אבל, אי אפשר לאפס את התלות ב-Windows היעד.

המאמר הזה מסדר את קו הגבול הזה, מכוון לעבודה מעשית עם יישומי Windows.

איחוד ל-EXE אחד אפשרי, אבל התלות ב-OS לא נעלמתתרשים המראה שבבקשה לאחד לבינארי יחיד נוטים להתערבב נושא ההפצה ונושא התלות, ושלמרות שאפשר במידה רבה לרכז את פריט ההפצה ל-EXE אחד, אי אפשר לאפס את התלות ב-Windows היעד.'רוצים בינארי יחיד'נושא ההפצה: רוצים פריט אחד, רוצים שרק יונחנושא התלות: בלי runtime, בלי תלות ב-OSאיחוד ל-EXE אחד אפשרי במידה רבהאי אפשר לאפס את התלות ב-OS

איור 1: ב’רוצים להפיץ בקובץ אחד’ מתערבבים דבר שאפשר ודבר שאי אפשר.

1. קודם כל, המסקנה

אם מסכמים קודם רק את המסקנה, זה כך:

  • ליישום שולחני רגיל, אפשר לאחד לבינארי יחיד עד רמה גבוהה למדי
  • עם זאת, היכולת לרכז ל-EXE אחד ו-היעדר תלות ב-Windows היעד הם שני דברים שונים
  • הרחבת מעטפת, שירות Windows, מנהל התקן,‏ WebView2, וחלק מ-WinUI 3 - עבורם, לרוב מה נרשם למערכת ההפעלה ומה מונח כתנאי מוקדם חשוב יותר ממספר הקבצים
  • החשוב ביותר בעבודה מעשית הוא להחליט בנפרד אם רוצים איחוד לבינארי יחיד, רוצים לבטל את הצורך במתקין, או רוצים להקטין את התלות ב-OS

במילים אחרות, קו הגבול ב-Windows הוא כך:

  • ריכוז פריט ההפצה לאחד: אפשרי במידה רבה
  • נשיאת runtime נוסף: אפשרי במידה רבה
  • התאמה להפצת xcopy: תלוי בסוג היישום
  • ביטול התלות בצד Windows היעד: בלתי אפשרי
קו הגבול ב-Windowsתרשים המראה שריכוז פריט ההפצה לאחד ונשיאת runtime נוסף אפשריים במידה רבה, שההתאמה להפצת xcopy תלויה בסוג היישום, ושביטול התלות בצד Windows בלתי אפשרי.ריכוז פריט ההפצה לאחדאפשרי במידה רבהצירוף runtime נוסףהתאמה להפצת xcopyתלוי בסוג היישוםביטול התלות בצד ה-OSבלתי אפשרי

איור 2: קו הגבול בין מה שאפשר, מה שתלוי בסוג, ומה שאי אפשר.

1.1 מונחים שבהם משתמש המאמר הזה

לפני הכול, מסדרים כאן רק את המילים שיחזרו הרבה בהמשך.

מונח קריאה/הרחבה המשמעות במאמר הזה
UCRT Universal C Runtime חלק ספריית ה-C הסטנדרטית שנוצר כשה-C runtime פוצל ב-Visual Studio 2015. מ-Windows 10 ואילך הוא מגיע כחלק ממרכיבי מערכת ההפעלה
חבילת ההפצה מחדש של Visual C++ Visual C++ Redistributable מתקין (installer) להתקנת DLL-ים של ה-runtime של Visual C++ מלבד UCRT, במחשב היעד
framework-dependent הפצה שתלויה ב-.NET שבסביבת היעד צורת הפצה שמניחה שה-runtime של‏ .NET מותקן במחשב היעד
self-contained צירוף ה-runtime של‏ .NET צורה שבה האפליקציה נושאת ומפיצה את מלוא ה-runtime של‏ .NET
single-file פרסום כקובץ יחיד אפשרות פרסום של‏ .NET שמרכזת את ההפצה ל-EXE אחד
Native AOT Native Ahead-Of-Time צורת הפצה שמקמפלת מראש את ה-IL לקוד נייטיבי בזמן הפרסום. לא משתמשת ב-JIT בזמן ריצה
הפצת app-local הפצה סמוכה לאפליקציה דרך הפצה שבה מניחים DLL באותה תיקייה כמו ה-EXE, ומשתמשים בו רק מתוך האפליקציה הזו
הפצת xcopy הפצה שכוללת רק העתקה דרך הפצה שפועלת בהנחת התיקייה כולה, בלי מתקין ובלי הרשאות מנהל
SCM Service Control Manager,‏ מנהל בקרת השירותים מנגנון של מערכת ההפעלה שמנהל רישום, הפעלה ועצירה של שירותי Windows
UAC User Account Control,‏ בקרת חשבון משתמש מנגנון אבטחה של Windows ששולט בהעלאת הרשאות למנהל
הרחבת מעטפת (Shell) shell extension רכיב COM שנטען ופועל בתוך תהליכים כמו Explorer
Evergreen שיטת עדכון מתמיד מצב הפצה שמפקיד את WebView2 Runtime בעותק משותף אחד שמתעדכן אוטומטית
Fixed Version שיטת גרסה קבועה מצב הפצה שמצרף גרסה מסוימת של WebView2 Runtime לאפליקציה עצמה
arch architecture,‏ ארכיטקטורת מעבד x86‏ / x64‏ / Arm64

מפת הידע של המאמר

עבור יישום Windows, ריכוז ההפצה ל-EXE אחד אפשרי במידה רבה כשלעצמו - אפשר להשתמש ב-self-contained,‏ single-file ו-Native AOT של .NET, ובקישור סטטי (/MT) של C/C++ - אך נשארת תלות בגרסת מערכת ההפעלה, בארכיטקטורה, ב-DLL-ים של המערכת ובמודל האבטחה, ואי אפשר לאפס את התלות ב-Windows היעד. self-contained ו-framework-dependent הן אפשרויות שמוציאות זו את זו, ו-Native AOT פועל בפועל כ-single-file אך במחיר של אי-יכולת להשתמש למשל ב-built-in COM. ב-WebView2 יש לבחור בין Evergreen ל-Fixed Version לאופן אספקת ה-runtime, וב-WinUI 3 האפשרות להשתמש ב-PublishSingleFile משתנה בהתאם ל-packaged(MSIX)לעומת unpackaged. מכיוון שהרחבת מעטפת ומנהלי התקן דורשים בעיקר תכנון של רישום וחתימה, לרוב הפצת app-local קלה יותר לתחזוקה מאשר לדחוס בכוח ל-EXE אחד.

מפת הידע של איחוד יישום Windows לבינארי יחידתרשים המראה שריכוז ההפצה לפריט אחד וביטול התלות במערכת ההפעלה הם שני צירים נפרדים, את אמצעי האיחוד לקובץ יחיד ב-.NET וב-C/C++ בהתאמה, ואיך אינטגרציה מארחת כמו WebView2 או WinUI 3 מגבילה את שיטת ההפצה עצמה.מחייבאינו מתיישב עםאינו מתיישב עםמחייבמחייבאינו מתיישב עםמחייבמשתמש במשתמש באינו מתיישב עםמשתמש במחייבמוגדר באמצעותמוגדר באמצעותאינו מתיישב עםמשתמש במחייבשימוש לא מומלץ למענה מומלץ למחייבמשתמש במחייבאיחוד לבינארי יחיד (הפצת קובץ אחד)תלות בגרסת Windows היעדפרסום self-containedפרסום framework-dependent‏ (‎.NET)Native AOTbuilt-in COM interopפרסום single-file‏ (‎.NET)קישור סטטי של ה-CRT‏ (‎/MT)קישור דינמי של ה-CRT‏ (‎/MD)חבילת ההפצה מחדש של Visual C++UCRT(Universal C Runtime)אריזת MSIX‏ (packaged)WinUI 3 / Windows App SDKMicrosoft Edge WebView2WebView2 Runtimeמצב הפצה Evergreen‏ (WebView2)זמן ריצה Fixed Versionהרחבת מעטפת (הרחבת סייר הקבצים)‏COM (Component Object Model)חתימת מנהל התקןחבילת מנהל התקןהפצה app-local (לצד קובץ ההרצה)

בתרשים, קו מלא מציין קשר שמתקיים תמיד וקו מקווקו מציין קשר מותנה (תנאי ההתקיימות מפורטים בהסבר של כל קשר בעמוד המפורט). רשימת כל הקשרים (סך הכול 22, עם אסמכתה ורמת ודאות) והגדרות המושגים המרכזיים מרוכזות בעמוד המפורט של מפת הידע (ביפנית). נתונים: JSON-LD / Turtle

2. חושבים על ‘בינארי יחיד’ בארבע רמות

אם מציירים את הרמות קודם, זה נראה כך. הקושי עולה מלמטה למעלה, והרמה העליונה ביותר לא ניתנת להשגה ב-Windows.

ארבע הרמות של בינארי יחידתרשים המראה את ארבע הרמות של בינארי יחיד, מלמטה למעלה - פריט הפצה אחד, ביטול התקנה מראש של runtime, ביטול התקנה או רישום, והיעדר תלות ב-Windows היעד שלא ניתן להשיג.רמה A - פריט הפצה אחד - אפשרי במידה רבהרמה B - ביטול התקנה מראש של runtime השפה - אפשרי במידה רבהרמה C - ביטול התקנה או רישום - תלוי בסוג היישוםרמה D - היעדר תלות ב-Windows היעד - לא ניתן להשיג ב-Windows

איור 3: ארבע הרמות של בינארי יחיד. הקושי עולה מלמטה למעלה, ואי אפשר להגיע לרמה D.

השיחה על ‘רוצים בינארי יחיד’ משתנה לגמרי בעבודה הנדרשת, בהתאם לאיזו משארבע הרמות היא מתכוונת.

2.1 רמה A: פריט הפצה אחד

זו הרמה השטחית ביותר.

  • אפשר לשלוח פריט אחד באימייל
  • מספיק להניח פריט אחד ב-USB
  • להניח רק את app.exe ביעד הפריסה

זהו סיפור של יחידת ההפצה החזותית. בפועל, גם אם יש פריסה זמנית בעת ההפעלה, וגם אם יש תלות ב-DLL-ים בצד ה-OS, אפשר לעמוד בתנאי הזה בלבד.

2.2 רמה B: אין צורך בהתקנה מראש של runtime השפה

הבא הוא מצב שבו האפליקציה פועלת גם בלי להתקין מראש במחשב היעד את ה-runtime של‏ .NET או את חבילת ההפצה מחדש של VC++.

  • קישור סטטי ב-C/C++
  • self-contained ב-.NET
  • single-file ב-.NET
  • Native AOT ב-.NET

ברמה הזו, התחושה של ‘אפשר לקחת לבד’ מתחזקת מאוד.

2.3 רמה C: אין צורך בהתקנה או רישום

מכאן זה נעשה קשה בפתאומיות.

אם זה EXE רגיל, לפעמים די להניח אותו כדי שיפעל. אבל הדברים הבאים שונים.

  • הרחבת מעטפת (Shell)
  • שירות Windows
  • תבנית URL מותאמת אישית או שיוך קבצים
  • מנהל התקן
  • רכיבים שנטענים לתהליכים אחרים, כמו Explorer או Office

בתחום הזה לא מספיק להניח קובץ. יש צורך ברישום בצד ה-OS ובחיבור אל הצד המארח (host).

תחומים שבהם לא מספיק להניח קובץתרשים המראה שהרחבת מעטפת, שירות Windows, שיוך קבצים, מנהל התקן ורכיבים שנטענים לתהליכים אחרים אינם מסתפקים בהנחת קובץ, ודורשים רישום בצד ה-OS וחיבור אל הצד המארח.הרחבת מעטפת, שירותלא מספיק להניח קובץשיוך, מנהל התקןדברים שנטענים לתהליכים אחריםדרוש רישום בצד ה-OS וחיבור אל המארח

איור 4: הקושי הפתאומי ברמה C נובע מכך שהתחום הזה דורש רישום ל-OS.

2.4 רמה D: היעדר תלות ב-Windows היעד

זה בלתי אפשרי ב-Windows.

הסיבה היא שיישום Windows פועל בסופו של דבר מעל ה-API, הטוען (loader), מודל האבטחה ומחסנית ההתקנים של Windows. איחוד לבינארי יחיד אפשרי רק עד גבול תחום האחריות של האפליקציה. הוא לא לוקח איתו את מערכת ההפעלה עצמה.

3. תחומים שקל יחסית לרכז ל-EXE אחד

גם ב-Windows יש יישומים שקל יחסית לרכז ל-EXE אחד.

  • כלי שולחני שמופעל באופן עצמאי
  • יישום עסקי שבו ה-EXE עצמו מכיל את ה-UI ואת העיבוד
  • כלים כמו תקשורת, טיפול בקבצים, איסוף לוגים, ניטור ובקרת ציוד
  • דברים שלא זקוקים לאינטגרציה מארחת עם Explorer או Office
  • UI שלא מניח runtime של אינטרנט

בסוג הזה, יש הרבה דברים שקל לכלול בגוף האפליקציה.

  • קוד עצמי
  • משאבים
  • מניפסט
  • הגדרות ברירת מחדל
  • נתוני תבנית
  • חלק מספריות צד שלישי
  • גוף ה-runtime של השפה

בנוסף, גם בלי להטמיע DLL לגמרי בתוך ה-EXE, הפצת app-local, שבה מניחים DLL לצד ה-EXE, היא אפשרות חזקה ורגילה ב-Windows. בעבודה מעשית,

  • app.exe אחד
  • או app.exe + כמה DLL-ים סמוכים
  • אבל בלי צורך במתקין, בלי הרשאות מנהל, ואפשר להפיץ ב-xcopy

לא נדיר שהצורה הזו קלה יותר לתחזוקה מאשר לדחוס בכוח ל-EXE אחד.

הפצת app-local כפתרון בינייםתרשים המראה שגם בלי להטמיע DLL לגמרי ב-EXE, אם מניחים DLL לצד ה-EXE ואפשר להפיץ ב-xcopy בלי מתקין ובלי הרשאות מנהל, לא נדיר שזה קל יותר לתחזוקה מאשר לדחוס בכוח ל-EXE אחד.app.exe + כמה DLL-ים סמוכיםבלי מתקין, בלי הרשאות מנהלאפשר להפיץ ב-xcopyלפעמים קל יותר לתחזוקה מדחיסה בכוח ל-EXE אחד

איור 5: גם בלי להתעקש על EXE אחד, אפשר לרוב להשיג את המטרה בעזרת הפצת app-local.

4. תלות ב-Windows שלא נעלמת גם עם EXE אחד

אם חושבים ש’EXE אחד אומר שאין תלות ב-Windows היעד’, כאן קורית התאונה. בפועל, גם אם מרכזים ל-EXE אחד, נשארת תלות.

4.1 תלות בגרסת מערכת ההפעלה

לכל API של Windows יש מערכת הפעלה מינימלית נתמכת. יש גם הבדל בין x64 ל-Arm64. כלומר, גם עם EXE יחיד,

  • האם זה פועל עד Windows 10
  • האם זה מניח Windows 11
  • האם צריך שזה יפעל גם ב-Windows Server
  • באיזה מבין x86‏/x64‏/Arm64 מכוונים

יש לקבע מראש.

4.2 תלות ב-DLL-ים של המערכת

גם אם התכוונו לרכז ל-EXE אחד, בזמן ריצה משתמשים כמובן ברכיבים שמספקת מערכת ההפעלה.

  • kernel32.dll
  • user32.dll
  • advapi32.dll
  • תשתית COM
  • תשתית בקרת שירותים

אלה בתחום האחריות של Windows.

4.3 תלות במודל האבטחה

  • UAC
  • ACL של קבצים
  • מנהל בקרת השירותים
  • Registry
  • מדיניות חתימת מנהלי התקן

דברים כאלה, האפליקציה לא יכולה לשאת לבדה.

תלות שנשארת גם עם EXE אחדתרשים המראה שגם עם EXE אחד, נשארת תלות במערכת ההפעלה המינימלית הנתמכת ובארכיטקטורה של ה-API, ב-DLL-ים של המערכת ותשתית COM כמו kernel32.dll, ובמודל האבטחה כמו UAC,‏ ACL ומדיניות חתימת מנהלי התקן.אפליקציית EXE אחדגרסת OS ו-archDLL-ים של המערכת ותשתית COMמודל האבטחהאף אחד מהם לא ניתן לשאת בצד האפליקציה

איור 6: גם כשמרכזים את ההפצה לפריט אחד, נשארת תלות בתחום האחריות של Windows.

4.4 תלות במארח (host) וב-runtime

אם התכנון אינו EXE שמופעל באופן עצמאי אלא נטען על גבי מארח (host) כלשהו, התלות גדלה בבת אחת.

  • משתמשים ב-WebView2: דרוש WebView2 Runtime
  • משתמשים ב-WinUI 3‏/Windows App SDK: דרוש סידור של מצב ההפצה
  • יוצרים הרחבת מעטפת: דרוש רישום בצד Explorer

כלומר, לרוב הבחירה ב-UI ובאינטגרציה הופכת ישירות לקושי ההפצה.

בחירת UI ואינטגרציה הופכת לקושי הפצהתרשים המראה שהשימוש ב-WebView2 דורש WebView2 Runtime, השימוש ב-WinUI 3 דורש סידור מצב הפצה, ויצירת הרחבת מעטפת דורשת רישום בצד Explorer - כך שבחירת המארח וה-runtime הופכת ישירות לקושי ההפצה.משתמשים ב-WebView2דרוש Runtimeמשתמשים ב-WinUI 3דרוש סידור מצב הפצהיוצרים הרחבת מעטפתדרוש רישום ב-Explorer

איור 7: ככל שמתרחקים מ-EXE שמופעל באופן עצמאי, התלות גדלה בבת אחת.

5. פתרונות ביניים מעשיים לפי טכנולוגיה

5.1 C/C++ נייטיבי

C/C++ נייטיבי נמצא בצד עם חופש גבוה לאיחוד לבינארי יחיד. יש מרווח לבחור בקישור סטטי, וב-EXE שמופעל באופן עצמאי קל למדי לרכז.

עם זאת, יותר מלדחוף הכול לקובץ אחד,

  • מה לעשות עם UCRT או ה-runtime של VC++
  • האם להניח DLL-ים של צד שלישי ב-app-local
  • עד כמה לצמצם את ה-CPU/OS היעד

אלה חשובים יותר בעבודה מעשית.

נקודת הכניסה: ההבדל בין /MT ל-/MD

ב-MSVC, מה שמכריע בפועל אם ‘לשאת runtime או לא’ הוא אפשרות המהדר (compiler) הזו.

אפשרות מה מקושר מה נדרש במחשב היעד
/MD ספריות ייבוא ל-DLL בשם ucrt.lib ו-vcruntime.lib. בזמן ריצה נעשה שימוש ב-ucrtbase.dll וב-vcruntime<גרסה>.dll UCRT וה-runtime של VC++
/MT קישור סטטי של libucrt.lib,‏ libvcruntime.lib,‏ libcmt.lib בלי runtime נוסף

הדרך המינימלית לנסות היא רק זו, ב-Developer Command Prompt.

cl /std:c++17 /EHsc /O2 /MT app.cpp /Fe:app.exe

יש כאן שלוש נקודות שכדאי לשים לב אליהן.

  • UCRT הפך ל-מרכיב של Windows כשה-C runtime פוצל ב-Visual Studio 2015, ומ-Windows 10 ואילך הוא מגיע כחלק ממערכת ההפעלה. אם מכוונים ל-Windows ישן יותר, דרושה הפצה מחדש באמצעות vcredist.
  • לא מומלץ לבנות DLL עם /MT. ה-CRT המקושר סטטית שומר את המצב שלו רק בתוך ה-DLL עצמו, ולכן הקצאת זיכרון,‏ locale, ואופן הפעולה של _set_se_translator סוטים בין ה-EXE ל-DLL. אם מיישרים בגסות “גם ה-EXE וגם ה-DLL המצורף ב-/MT”, קורית תאונה בהקצאה/שחרור שחוצה את הגבול.
  • אי אפשר לשלב /clr עם /MT. אם משלבים C++/CLI, יש ללכת לצד /MD.
שלוש נקודות זהירות בקישור סטטיתרשים המראה שUCRT מגיע כחלק מ-Windows 10 ואילך אך ב-Windows ישן יותר דרושה הפצה מחדש עם vcredist, שבניית DLL עם /MT סוגרת את מצב ה-CRT בתוך ה-DLL וגורמת לתאונה בהקצאה/שחרור שחוצה גבול, ושאי אפשר לשלב /clr עם /MT./MTבקישור סטטיב-Windows ישן דרוש vcredist/MT ב-DLL לא מומלץאי אפשר לשלב עם /clrתאונה בהקצאה/שחרור שחוצה גבול

איור 8: /MT חזק, אבל יש להיזהר משלוש הנקודות - הנחת UCRT, גבול ה-DLL, ו-C++/CLI.

5.2 .NET

ל-.NET יש single-file,‏ self-contained ו-Native AOT, ולכן אפשר להקטין משמעותית את יחידת ההפצה החזותית.

עם זאת, יש צורך להבחין.

  • framework-dependent: תלוי ב-.NET שבסביבת היעד
  • self-contained: נושא את ה-runtime של‏ .NET
  • single-file: מרכז את ההפצה לפריט אחד
  • Native AOT: מקטין עוד יותר את התלות בזמן ההפעלה, אך יש גם מגבלות תפקוד

אין זה ש’בגלל single-file, התלות ב-OS קטנה’. מה שקטן הוא בעיקר ריכוז פריט ההפצה של האפליקציה.

מה ש-single-file מקטין הוא ריכוז ההפצהתרשים המראה ש-framework-dependent תלוי ב-.NET שבסביבת היעד, ש-self-contained נושא את ה-runtime, וש-single-file רק מרכז את ההפצה לפריט אחד - ושלא בגלל single-file התלות ב-OS קטנה.framework-dependentתלוי ב-.NET שבסביבת היעדself-containedנושא את ה-runtime של .NETsingle-fileמרכז את ההפצה לפריט אחדלא אומר שהתלות ב-OS קטנה

איור 9: בכל צורת פרסום של‏ .NET, מה שקטן ומה שנשאר שונים.

נקודת הכניסה: שלוש התבניות של dotnet publish

אם רוצים קודם להפעיל ולראות את ההבדל, שלוש הפקודות האלה מספיקות.

rem 1. framework-dependent + single-file: דרוש runtime של .NET במחשב היעד
dotnet publish -c Release -r win-x64 --self-contained false -p:PublishSingleFile=true

rem 2. self-contained + single-file: נושא את ה-runtime של .NET
dotnet publish -c Release -r win-x64 --self-contained true -p:PublishSingleFile=true

rem 3. Native AOT: לאחר כתיבת PublishAot ב-csproj, מפרסמים
dotnet publish -c Release -r win-x64

ההמלצה הרשמית היא לכתוב את PublishSingleFile ואת PublishAot ב-csproj ולא בשורת הפקודה. כך מופעל ניתוח תאימות במהלך ה-build, וזה מקטין תאונות שמתגלות רק אחרי הפרסום.

<PropertyGroup>
  <PublishSingleFile>true</PublishSingleFile>
  <PublishAot>true</PublishAot>
</PropertyGroup>

בנוסף, אם מציינים RuntimeIdentifier,‏ SelfContained הופך ל-true כברירת מחדל. כשרוצים framework-dependent, יש לציין במפורש --self-contained false. כדי לפרסם Native AOT ב-Windows, דרוש עומס העבודה (workload) פיתוח שולחני עם C++ של Visual Studio.

פשרה: ‘לרכז לאחד’ אינו בחינם

אם כותבים כאן רק ‘אפשר’, קורית תאונה, ולכן מסדרים גם את המחיר.

פריסה בזמן ההפעלה של single-file

  • כברירת מחדל, מה שנארז (bundled) הוא רק DLL-ים מנוהלים (managed). הם נטענים לזיכרון בזמן ההפעלה ולא נפרסים לתיקייה. הבינארי הנייטיבי של גוף ה-runtime נשאר כקובץ נפרד.
  • אם רוצים לכלול גם את הבינארי הנייטיבי בפריט אחד, משתמשים ב-IncludeNativeLibrariesForSelfExtract; אם רוצים לפרוס הכול לפני ההרצה, משתמשים ב-IncludeAllContentForSelfExtract. בשני המקרים ההתנהגות היא “פורסים ואז מפעילים”, וב-Windows הפריסה היא תחת %TEMP%\.net. אפשר לשנות את המיקום עם DOTNET_BUNDLE_EXTRACT_BASE_DIR, אבל אין להפוך אותו למיקום שניתן לכתיבה על ידי משתמשים או שירותים בעלי הרשאות שונות.
  • אם מפעילים EnableCompressionInSingleFile, ה-EXE נעשה קטן משמעותית, אבל ההפעלה מואטת בגלל הפענוח (decompression) בזיכרון בזמן ההפעלה. גם המסמכים הרשמיים כותבים “למדדו גם גודל וגם עלות הפעלה לפני השימוש”. ההשפעה משתנה מאוד בין אפליקציות.

אי-תאימות API ב-single-file

כשמרכזים את ההפצה לפריט אחד, קוד שמניח נתיב קובץ נשבר בשקט.

API ההתנהגות ב-single-file
Assembly.Location מחזיר מחרוזת ריקה
Assembly.CodeBase PlatformNotSupportedException
Assembly.GetFile IOException
Module.Name מחזיר את המחרוזת <Unknown>

אם נוגעים בקובץ שלצד ה-EXE, יש לעבור ל-AppContext.BaseDirectory; אם דרוש נתיב קובץ ההרצה, ל-Environment.ProcessPath. מכיוון שספריות צד שלישי משתמשות בהם לפעמים גם באופן פנימי, אחרי ההמרה ל-single-file דרושה בדיקת הפעלה על מכשיר אמיתי לפחות פעם אחת.

קוד שמניח נתיב נשבר בשקטתרשים המראה שכשמרכזים את ההפצה לפריט אחד, התנהגות API-ים שמניחים נתיב קובץ משתנה - כמו Assembly.Location שמחזיר מחרוזת ריקה - ולכן יש לעבור אל AppContext.BaseDirectory לקובץ הסמוך ואל Environment.ProcessPath לנתיב קובץ ההרצה, ולבדוק הפעלה על מכשיר אמיתי לפחות פעם אחת.המרה ל-single-fileהתנהגות API-ים שמניחים נתיב משתנהקובץ סמוך: AppContext.BaseDirectoryקובץ הרצה: Environment.ProcessPathבדיקת הפעלה על מכשיר אמיתי

איור 10: אחרי ההמרה ל-single-file, מחליפים את אופן קבלת הנתיב ובודקים על מכשיר אמיתי.

מגבלות התפקוד של Native AOT

במקום ‘הפעלה מהירה, בלי runtime’, הדברים הבאים הופכים לבלתי זמינים.

  • טעינה דינמית, כמו Assembly.LoadFile
  • יצירת קוד בזמן ריצה, כמו System.Reflection.Emit
  • C++/CLI
  • ב-Windows,‏ built-in COM
  • trimming הוא חובה, ולכן גם מגבלות ה-trimming חלות במלואן
  • בפועל זה single-file, ולכן גם אי-התאימות ה-API שלמעלה חלה במלואה
  • System.Linq.Expressions תמיד פועל כמפרש (interpreter), ואיטי יותר מקוד שנוצר בזמן ריצה

גם הפלטפורמה היעד קבועה מראש. ב-.NET 8, ב-Windows נתמכים x64 ו-Arm64, ומ-.NET 9 ואילך נוסף גם x86. הרעיון של ‘פריט אחד ל-AnyCPU’ לא מתקיים מלכתחילה.

המחיר של Native AOTתרשים המראה ש-Native AOT מקטין את התלות בזמן ההפעלה, אך במחיר של אי-אפשרות לטעינה דינמית, יצירת קוד בזמן ריצה, C++/CLI ו-built-in COM ב-Windows, שגם מגבלות ה-trimming וה-single-file חלות, ושהפלטפורמה היעד קבועה מראש.Native AOTמקטין תלות בזמן ההפעלהטעינה דינמית ויצירת קוד בזמן ריצה - בלתי אפשרייםC++/CLI ו-built-in COM - בלתי אפשרייםחלות גם מגבלות ה-trimming וה-single-fileOS ו-arch היעד קבועים מראש

איור 11: יש לראות את היתרונות של Native AOT יחד עם הפונקציות שהופכות לבלתי זמינות.

5.3 WebView2

כשמאמצים את WebView2, קושי ה-single binary משתנה לגמרי. הנושא המרכזי כאן הוא לא מספר ה-EXE-ים, אלא איך לטפל ב-WebView2 Runtime.

לפני ‘האם אפשר לרכז ל-EXE אחד’, יש שאלה שכדאי לחשוב עליה.

  • האם להניח שה-runtime כבר קיים בסביבה
  • האם להשתמש ב-Evergreen
  • האם לצרף Fixed Version
  • עד כמה לקחת אחריות בהפצת offline

בגדול, ההבדל בין Evergreen ל-Fixed Version הוא כך.

  Evergreen Fixed Version
מי מעדכן Microsoft. מתעדכן אוטומטית אני. מוחלף בהתאם לעדכון האפליקציה
הישות במחשב עותק אחד משותף לכל האפליקציות מצורף לכל אפליקציה בנפרד
גודל ההפצה כמעט לא גדל עולה על 250 MB
דרך ההתקנה ה-Bootstrapper הוא כ-2 MB ומוריד את מה שדרוש. ב-offline מצרפים את ה-Standalone Installer מפיצים את מלוא הבינארי הפרוס יחד עם האפליקציה
מגבלה קיימת יש לבדוק לפני ההפעלה אם זה מותקן במחשב אי אפשר להריץ מנתיב רשת או UNC

Evergreen Runtime מותקן מראש כחלק ממערכת ההפעלה ב-Windows 11. אבל מכיוון שיש עדיין מחשבי Windows 10 שבהם הוא לא מותקן, גם Microsoft עצמה כותבת ש’גם אם בוחרים ב-Evergreen, כדאי בכל זאת להפיץ את ה-Runtime’. כלומר, בכל מקרה דרוש נוהל שבו האפליקציה בודקת אם הוא קיים, ואם לא מספיק - מתקינה אותו. Fixed Version מאפשר ‘לאחוז בעצמך בתזמון העדכון’, אבל במחיר של גידול הפצה של מאות MB. יש להחליט על כך מראש, בנפרד מהשאלה של EXE אחד.

מחליטים מראש על אופן ההפצה של WebView2תרשים המראה ש-Evergreen הוא עותק משותף אחד שמעדכנת Microsoft אוטומטית אך לפעמים לא מותקן במחשב, ש-Fixed Version מאפשר לאחוז בעדכון בעצמך אך מגדיל את ההפצה במאות MB, ושבשני המקרים דרוש נוהל שבודק קיום ומתקין במידת הצורך.מאמצים את WebView2Evergreen: עותק משותף שמתעדכן אוטומטיתFixed Version: מצרפים ומעדכנים בעצמנובודקים קיום, מתקינים אם חסרההפצה גדלה במאות MB

איור 12: הנושא המרכזי של WebView2 אינו מספר ה-EXE-ים, אלא הטיפול ב-Runtime.

5.4 WinUI 3 / Windows App SDK

גם WinUI 3 - ברגע שמאמצים אותו, דרישות ההפצה משתנות. הבחירה בטכנולוגיית UI הופכת ישירות לבחירת שיטת ההפצה.

באופן ספציפי, יש שני צירים להחליט עליהם.

  • packaging: packaged‏(MSIX), packaged שמצביע למיקום חיצוני, או unpackaged
  • runtime: framework-dependent או self-contained

ומה שמשפיע מנקודת המבט של single binary היא מגבלת השילוב הזו.

  • PublishSingleFile ניתן לשימוש רק ביישום WinUI 3 שהוא unpackaged וגם self-contained, ודרוש Windows App SDK 1.5 ואילך.
  • ביישום packaged, וכן ביישום packaged שמצביע למיקום חיצוני, אי אפשר להשתמש ב-PublishSingleFile.
  • אם עוברים ל-unpackaged, מאבדים את package identity, ולכן פונקציות Windows שמניחות package identity - כמו התראות, משימות רקע, שיוך קבצים והרחבת תפריט הקשר - הופכות לבלתי זמינות.

כלומר, ב-WinUI 3, ‘לרכז ל-EXE אחד’ ו’להשתמש בפונקציות ההרחבה של Windows’ מתנגשים חזיתית. אם מעדיפים single binary מעל הכול, לרוב מהיר יותר לבחון מחדש מלכתחילה את ההנחות של טכנולוגיית ה-UI.

ההתנגשות בין EXE אחד ל-package identityתרשים המראה שב-WinUI 3, PublishSingleFile ניתן לשימוש רק בתצורת unpackaged וגם self-contained, ושהמעבר ל-unpackaged גורם לאובדן package identity ולביטול פונקציות כמו התראות ושיוך קבצים, כך ש-EXE אחד ופונקציות ההרחבה של Windows מתנגשים חזיתית.רוצים להשתמש ב-PublishSingleFileרק unpackaged + self-containedאיבוד package identityהתראות, שיוך וכו' לא זמיניםEXE אחד מתנגש חזיתית עם פונקציות ההרחבה

איור 13: ב-WinUI 3, הבחירה ב-EXE אחד פירושה לוותר על package identity.

6. תחומים שבהם רישום ותלות דרושים מיסודם

6.1 הרחבת מעטפת (Shell)

הרחבת מעטפת שנטענת ל-Explorer שונה לחלוטין מ-‘EXE שרק מניחים’. כאן, יותר ממספר הקבצים, הנושא המרכזי הוא איך רושמים ל-Explorer.

6.2 שירות Windows

גם אם אפשר לרכז את ה-exe של גוף השירות עצמו לקובץ אחד, ההפצה היא בעיה נפרדת.

  • רישום ב-SCM
  • הרשאות
  • חשבון ההפעלה
  • הגדרות התאוששות

יש לחשוב עליהם. כלומר, שירות הוא תחום שבו יותר מ-‘לרכז ל-EXE אחד’, יש לגבש את ‘איך מתקינים’.

6.3 מנהל התקן

מנהל התקן ברור אף יותר. הוא מתקיים רק כולל INF, חתימה ותהליך התקנה, ולכן מלכתחילה קשה לו לעלות על זירת ה-single binary.

תחומים שבהם רישום וחתימה הם הנושא המרכזיתרשים המראה שהרחבת מעטפת מתקיימת עם רישום ל-Explorer, שירות Windows מתקיים עם רישום ב-SCM והרשאות וחשבון הפעלה, ומנהל התקן מתקיים עם INF וחתימה - כך שיותר ממספר הקבצים, תכנון הרישום והתלות הוא הנושא המרכזי.הרחבת מעטפתרישום ב-Explorerשירותרישום ב-SCM, הרשאות, חשבוןמנהל התקןINF וחתימהתכנון הרישום חשוב יותר ממספר הקבצים

איור 14: בשלושת התחומים האלה, יותר מ-‘לרכז ל-EXE אחד’, מגבשים ‘איך רושמים’.

7. טבלת החלטה מעשית

לצורך הכרעה גסה, הטבלה הזו נוחה לשימוש.

מה רוצים לבנות מידת המימוש של EXE אחד מה כדאי לשקול מראש
כלי Win32‏/C++ שמופעל באופן עצמאי גבוהה קישור סטטי,‏ OS/arch יעד
כלי WinForms‏/WPF שמופעל באופן עצמאי גבוהה התאמת self-contained,‏ single-file,‏ Native AOT
יישום WinUI 3‏/Windows App SDK בינונית מצב הפצה, תלות נוספת
UI שולחני מבוסס WebView2 נמוכה עד בינונית שיטת הפצת ה-Runtime
הרחבת קליק ימני או תצוגה מקדימה ב-Explorer נמוכה רישום COM‏/Registry
שירות Windows בינונית רישום SCM, הרשאות, נוהל עדכון
יישום עם מנהל התקן מצורף נמוכה INF, חתימה, התקנה

החשוב ביותר בטבלה הזו הוא להבין ש-‘מספר הבינאריים’ ו-‘תחום האחריות של ההפצה’ הם שני דברים שונים.

8. מה כדאי להחליט מראש בתכנון ההפצה

אם רוצים להצליח באיחוד לבינארי יחיד, יש דברים שכדאי להחליט לפני היישום.

8.1 מחליטים מה רוצים לרכז לאחד

  • האם רוצים לרכז את פריט ההפצה לאחד
  • האם רוצים לבטל התקנה מראש של runtime
  • האם רוצים לבטל צורך במתקין
  • האם רוצים להקל על עדכון offline

בהתאם לתשובה, הטכנולוגיה שבוחרים משתנה.

8.2 מקבעים מראש את Windows המינימלי הנתמך ואת ה-arch

גם single-file וגם Native AOT הם ביסודם OS/architecture specific. אם ממשיכים ב-‘בכל מקרה, בקובץ אחד’ בלי להבהיר את זה, בסוף נתקעים בגלל חוסר API או אי-התאמת runtime.

8.3 מנסחים במפורש ‘מה מצרפים’ ו’מה משאירים ל-Windows’

בעבודה מעשית, רק כתיבת הרשימה הזו מקטינה משמעותית תאונות.

  • מה שמצרפים לאפליקציה
    • ה-exe עצמו
    • DLL עצמי
    • תבנית הגדרות
    • self-contained runtime
  • מה שמשאירים ל-Windows
    • DLL-ים של המערכת
    • OS API
    • SCM‏/Registry‏/Explorer
    • תשתית מנהלי התקן
  • מה שמניחים כתנאי מוקדם בנפרד
    • WebView2 Runtime
    • VC++ Redistributable
    • Office / Excel
    • מנהל התקן ייעודי
מנסחים במפורש שלוש קטגוריות של תחום אחריותתרשים המראה שרק כתיבת שלוש הקטגוריות - מה שמצרפים לאפליקציה, מה שמשאירים ל-Windows, ומה שמניחים בנפרד - מקטינה משמעותית תאונות הפצה.תחום האחריות של ההפצהדברים שמצרפים לאפליקציהדברים שמשאירים ל-Windowsדברים שמניחים בנפרדרק כתיבתם מקטינה תאונות

איור 15: מנסחים מראש את שלוש הקטגוריות - צירוף, השארה ל-OS, והנחה בנפרד.

8.4 אם מעדיפים single binary, מקטינים אינטגרציה מארחת

זה משפיע משמעותית.

  • מוותרים על הרחבת מעטפת והופכים ל-EXE רגיל
  • לא הופכים לשירות, ומסתפקים ב-Task Scheduler או הפעלה מפורשת
  • משתמשים ב-UI נייטיבי במקום WebView2
  • סוגרים את ה-COM בתוך התהליך העצמי

בקיצור, ככל שמקטינים תכנון של ‘לגרום ל-OS לטעון’ או ‘לרשום ל-OS’, מתקרבים ל-single binary.

ככל שמקטינים אינטגרציה מארחת, מתקרביםתרשים המראה שוויתור על הרחבת מעטפת לטובת EXE רגיל, אי-הפיכה לשירות והסתפקות ב-Task Scheduler או הפעלה מפורשת, ושימוש ב-UI נייטיבי במקום WebView2 - כל אלה מקטינים תכנון של טעינה ורישום ל-OS, ומתקרבים ל-single binary.מוותרים על הרחבת מעטפת, EXE רגילמקטינים תכנון רישום ל-OSלא הופכים לשירות, הפעלה מפורשתUI נייטיבי במקום WebView2מתקרבים ל-single binary

איור 16: אם מעדיפים single binary, מקטינים את האינטגרציה המארחת עצמה.

9. סיכום

איחוד לבינארי יחיד ב-Windows אפשרי עד רמה גבוהה למדי. אבל למה זה מגיע, זה במשפט הזה.

אפשר להפוך את האפליקציה ל-EXE אחד. אבל אי אפשר להפוך גם את Windows שבו האפליקציה תלויה ל-EXE אחד.

חמש נקודות שכדאי לזכור במיוחד:

  • ל-EXE רגיל שמופעל באופן עצמאי, אפשר לרכז לרמה משמעותית להפצה בקובץ אחד
  • קישור סטטי ב-C/C++,‏ single-file ב-.NET, ו-Native AOT הם אפשרויות חזקות
  • אבל התלות בגרסת OS,‏ arch,‏ DLL-ים של המערכת, ומודל האבטחה לא נעלמת
  • הרחבת מעטפת, שירות, מנהל התקן,‏ WebView2, וחלק מ-WinUI 3 - עבורם, נושא הרישום ל-OS או ה-runtime הנוסף הוא גוף העניין
  • ההצלחה או הכישלון של single binary נקבעים בהפרדה מראש של ‘מה רוצים לרכז לאחד’

אם מעדיפים single binary בתוקף, קל בהרבה להצליח אם מתכננים כבר בשלב בחירת הטכנולוגיה בכיוון של הורדת מידת הצימוד ל-OS.

המפתח להצלחה הוא מידת הצימוד ל-OSתרשים המראה שאפשר להפוך אפליקציה ל-EXE אחד, אך אי אפשר להפוך גם את Windows שהיא תלויה בו ל-EXE אחד, ולכן אם מעדיפים single binary בתוקף, כדאי להוריד את מידת הצימוד ל-OS כבר בשלב בחירת הטכנולוגיה.הופכים את האפליקציה ל-EXE אחדאפשריהופכים גם את Windows התלוי ל-EXE אחדבלתי אפשרילכן מורידים את הצימוד ל-OS בבחירת הטכנולוגיה

איור 17: ההצלחה של איחוד ל-EXE אחד נקבעת בתכנון מידת הצימוד ל-OS מלכתחילה.

מקורות

מאמרים עדכניים עם אותן תגיות, להעמקה בנושאים קרובים.

העמודים האלה ממקמים את הנושא בהקשר רחב יותר של שירותים והחלטות.

המאמר קשור ישירות לשירותים הבאים.

שאלות נפוצות

שאלות נפוצות בפניות בנושא המאמר.

האם אפשר להפיץ יישום Windows בקובץ EXE אחד בלבד?
עבור כלי שולחני שמופעל באופן עצמאי, אפשר להגיע לרמה גבוהה למדי. באמצעות קישור סטטי ב-C/C++,‏ self-contained או single-file ב-.NET, או Native AOT, אפשר לרכז את ההפצה ל-EXE אחד. עם זאת, היכולת לרכז ל-EXE אחד והיעדר תלות ב-Windows היעד הם שני דברים שונים, והתלות בגרסת מערכת ההפעלה, הארכיטקטורה,‏ DLL-ים של המערכת ומודל האבטחה לא נעלמת.
האם המרה ל-single-file ב-.NET מבטלת את התלות במערכת ההפעלה?
לא. מה ש-single-file מקטין הוא בעיקר את הריכוז של פריט ההפצה של האפליקציה, ולא את התלות במערכת ההפעלה. framework-dependent תלוי ב-.NET שבסביבת היעד, self-contained נושא איתו את ה-runtime של‏ .NET, ו-Native AOT יכול להקטין עוד יותר את התלות בזמן ההפעלה אך יש לו גם מגבלות תפקוד. גם single-file וגם Native AOT הם ביסודם ספציפיים למערכת הפעלה וארכיטקטורה, ולכן יש לקבע מראש את גרסת Windows המינימלית הנתמכת ואת ה-arch.
אילו יישומים קשה לרכז ל-EXE אחד?
הרחבות מעטפת (Shell), שירותי Windows, מנהלי התקן, ממשקי משתמש מבוססי WebView2, וחלק מ-WinUI 3. עבור אלה, לא מספר הקבצים הוא הנושא המרכזי, אלא הרישום למערכת ההפעלה והטיפול ב-runtime נוסף. לדוגמה, הרחבת מעטפת דורשת רישום ב-Explorer, שירות דורש תכנון של רישום ב-SCM, הרשאות וחשבון ההפעלה, ומנהל התקן נשלם רק כולל INF וחתימה - ולכן הפצה של 'רק להניח קובץ' לא מתקיימת. עבור WebView2 יש להחליט מראש על אופן ההפצה של WebView2 Runtime.
האם יש שיטת הפצה טובה יותר מאשר להכריח הטמעה ל-EXE אחד?
הפצת app-local, שבה מניחים DLL לצד ה-EXE, היא אפשרות חזקה. גם בצורה של app.exe וכמה DLL-ים סמוכים, אם אפשר להפיץ ב-xcopy בלי צורך בהתקנה ובלי הרשאות מנהל, לא נדיר שקל יותר לתחזק מאשר לדחוס בכוח ל-EXE אחד. החשוב הוא להפריד מראש בין הרצון לרכז את ההפצה לפריט אחד, הרצון לבטל התקנה מראש של runtime, והרצון לבטל צורך במתקין (installer).

פרופיל הכותב

עמוד היכרות עם כותב המאמר.

Go Komura

מנהל KomuraSoft LLC

מתמחה בפיתוח תוכנה עבור Windows, ייעוץ טכני וחקירת תקלות, בעיקר בפרויקטים עם מערכות קיימות ובאגים שקשה לשחזר.

קישורים ציבוריים

חזרה לבלוג