לדלג לתוכן

בנייה והפצה ניתנות לשחזור

נניח שהתוכנית פועלת במחשב הפיתוח אך נכשלת במחשב נקי. קל להאשים את Windows, אבל מחשב הפיתוח מכיל SDK, runtimeים, משתני סביבה וקבצים שאינם חלק מההפצה. הצלחה מקומית יכולה להסתיר תלות שלא ידענו שיש לנו.

המטרה בשיעור הזה היא להפוך את תהליך הבנייה לרשימת קלטים ותוצרים שאפשר לבדוק. שחזור בנייה אומר שאפשר להפיק תוצר בתנאים מוגדרים. שחזור התנהגות דורש בנוסף קלט, מערכת יעד ותצורת סביבה. זהות ביט־לביט היא יעד חזק נוסף, שאינו נובע אוטומטית משני הראשונים.

חבילה קטנה עם שתי צורות קישור

נשתמש ב־calc וב־linked מהשיעור 3.5. שמרו את המקור בתיקייה ייעודית, והוסיפו קובץ CMakeLists.txt.

cmake_minimum_required(VERSION 3.20)
project(CalculatorPackage VERSION 1.0 LANGUAGES CXX)

add_library(calc SHARED calc.cpp)
target_compile_features(calc PRIVATE cxx_std_20)

add_executable(linked linked.cpp)
target_compile_features(linked PRIVATE cxx_std_20)
target_link_libraries(linked PRIVATE calc)

if(MSVC)
    target_compile_options(calc PRIVATE /W4 /EHsc)
    target_compile_options(linked PRIVATE /W4 /EHsc)
    set_property(TARGET calc linked PROPERTY
        MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>DLL")
endif()

install(TARGETS calc linked RUNTIME DESTINATION bin)

הקובץ אינו קובע בשבילנו איזו גרסת Visual Studio או SDK מותקנת. את הבחירה הזו נועלים בסביבת העבודה או ב־preset שמותאם למחזור. אין להשתמש במחרוזת latest כהבטחה לשחזור, מפני שמשמעותה משתנה.

בחלון כלי הבנייה המתאים מפעילים את CMake. בוחרים generator מותקן ומתעדים אותו; בדוגמה הבאה משתמשים ב־Ninja אם הוא זמין.

cmake -S . -B build-release -G Ninja -DCMAKE_BUILD_TYPE=Release
cmake --build build-release --verbose
cmake --install build-release --prefix package

הפלט המפורט מאפשר לראות את פקודות המהדר וה־linker בפועל. חשוב לבדוק אותן, מפני שהגדרות פרויקט יכולות להתווסף ממספר מקומות.

מתעדים את מה שנכנס ואת מה שיצא

ה־manifest של הבנייה צריך לכלול revision של המקור, יעד ארכיטקטורה, גרסאות כלים, תלויות, flags וזהות התוצרים. אין הכרח לחשוף מידע אישי כמו כל משתני הסביבה; אוספים רק קלטים שרלוונטיים לבנייה ולתחקיר.

הקטע הבא יוצר רשימת hashes של חבילת ההפצה בתוך תיקיית הניסוי. הוא אינו בונה את התוכנית ואינו משנה קבצי מערכת.

$packageRoot = (Resolve-Path -LiteralPath '.\package').Path
$entries = Get-ChildItem -LiteralPath $packageRoot -Recurse -File |
    ForEach-Object {
        $digest = Get-FileHash -LiteralPath $_.FullName -Algorithm SHA256
        [pscustomobject]@{
            path = $_.FullName.Substring($packageRoot.Length + 1)
            bytes = $_.Length
            sha256 = $digest.Hash
        }
    }
$entries | ConvertTo-Json -Depth 4 |
    Set-Content -LiteralPath '.\package-files.json' -Encoding utf8

ה־hash מזהה תוכן, אך אינו מוכיח מי יצר אותו. חתימה ואמון יידונו בהמשך. גם זהות hashes של שני builds אינה מוכיחה שהתוכנית נכונה; היא רק נותנת ראיה לגבי זהות התוצרים.

בודקים runtime כחלק מהחוזה

הבנייה בדוגמה משתמשת ב־CRT דינמי. לכן ההפצה צריכה להגדיר כיצד runtime נתמך מגיע למערכת היעד. בודקים את מדיניות ההפצה הרשמית של גרסת הכלים, ולא מעתיקים DLL מקרי מתוך תיקיית פיתוח.

אין להפיץ debug runtime כמוצר רגיל מתוך הנחה שהוא זמין ללקוח. בחירת /MT משנה חלק מהתלויות, אך אינה מבטלת את הצורך להבין עדכונים, בעלות וגבולות CRT. היא גם אינה הופכת DLL שמקצה ומשחרר זיכרון בצדדים שונים לבטוח אוטומטית.

כלי dumpbin /dependents מציג שמות תלויות ישירות. הוא אינו בהכרח מגלה כל טעינה דינמית שתתרחש מאוחר יותר, ואינו מציג את כל החלטות החיפוש בזמן ריצה. לכן משלבים ניתוח סטטי עם ניסוי אמיתי במערכת יעד.

מכונה נקייה חושפת הנחות

ב־VM או בסביבת בדיקה נפרדת מעתיקים רק את החבילה ואת רכיבי ה־runtime שהוגדרו. מפעילים מתוך תיקיית עבודה שונה, עם משתמש רגיל וללא SDK. התוכנית צריכה למצוא את תלויותיה ולדווח שגיאות בצורה ברורה.

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

לקוח ישן הוא נכס בדיקה

שמרו את ה־EXE הראשון של הלקוח. שנו את מימוש calc, בנו רק את ה־DLL והפעילו את הלקוח הישן. זו בדיקת תאימות בינארית והתנהגותית אמיתית יותר מבנייה מחדש של שניהם.

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

מגדירים מה הצלחנו לשחזר

דוח הבנייה צריך להבחין בין שלוש טענות. הטענה הראשונה היא שאפשר לבנות ממקור שמור. השנייה היא שהתוצר תואם ביט־לביט לתוצר קודם. השלישית היא שהתוצר מתנהג באותה צורה בתרחישי יעד שהוגדרו.

חותמות זמן, נתיבי PDB, חתימה דיגיטלית ונתוני אופטימיזציה יכולים להשפיע על bytes. תנאי מערכת וקלט יכולים להשפיע על התנהגות. פירוט ההבדל אינו תירוץ לחוסר סדר, אלא הדרך לבנות בדיקה שמודדת את הדבר הנכון.

לקריאה נוספת: הפצת רכיבי Visual C++.