Move from shipping the initrd to generating it during install. (#14119)

* Move from shipping the initrd to generating during package install. * pr feedback * working * adjust custom action conditions * update initrd test to cover more cases * Update msipackage/package.wix.in Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * use stack buffer * move initrd helper to filesystem.cpp and add unit test --------- Co-authored-by: Ben Hillis <benhill@ntdev.microsoft.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

Ben Hillis committed Feb 7, 2026 at 12:47 UTC 0e263df6f36c71ee8cdb84406620458047316ba8
16 files changed +313 -310
.pipelines/build-stage.yml
+1 -1
@@ -23,7 +23,7 @@ parameters:
23 - name: targets
24 type: object
25 default:
26 - - target: "wsl;libwsl;wslg;wslservice;wslhost;wslrelay;wslinstaller;wslinstall;initramfs;wslserviceproxystub;wslsettings;wslinstallerproxystub;testplugin"
26 + - target: "wsl;libwsl;wslg;wslservice;wslhost;wslrelay;wslinstaller;wslinstall;init;wslserviceproxystub;wslsettings;wslinstallerproxystub;testplugin"
27 pattern: "wsl.exe,libwsl.dll,wslg.exe,wslservice.exe,wslhost.exe,wslrelay.exe,wslinstaller.exe,wslinstall.dll,wslserviceproxystub.dll,wslsettings/wslsettings.dll,wslsettings/wslsettings.exe,wslinstallerproxystub.dll,wsldevicehost.dll,WSLDVCPlugin.dll,testplugin.dll,wsldeps.dll"
28 - target: "msixgluepackage"
29 pattern: "gluepackage.msix"
CMakeLists.txt
-2
@@ -66,7 +66,6 @@ find_nuget_package(Microsoft.WSL.bsdtar BSDTARD /build/native/bin)
66 find_nuget_package(Microsoft.WSL.LinuxSdk LINUXSDK /)
67 find_nuget_package(Microsoft.WSL.TestDistro TEST_DISTRO /)
68 find_nuget_package(Microsoft.WSLg WSLG /build/native/bin)
69 -find_nuget_package(StrawberryPerl PERL /)
69 find_nuget_package(vswhere VSWHERE /tools)
70 find_nuget_package(Wix WIX /tools/net6.0/any)
71
@@ -92,7 +91,6 @@ find_nuget_package(Microsoft.Windows.SDK.NET.Ref WINDOWS_SDK_DOTNET /)
91 find_nuget_package(Microsoft.Xaml.Behaviors.WinUI.Managed XAML_BEHAVIORS /)
92 find_nuget_package(WinUIEx WINUIEX /)
93
95 -set(BIN2CPIO ${CMAKE_CURRENT_LIST_DIR}/tools/bin2cpio/bin2cpio.pl)
94 set(WSLG_TS_PLUGIN_DLL "WSLDVCPlugin.dll")
95
96 # Default to debug build if unspecified
msipackage/CMakeLists.txt
+2 -2
@@ -22,7 +22,7 @@ foreach(binary ${WINDOWS_BINARIES})
22 list(APPEND BINARIES_DEPENDENCIES "${PACKAGE_INPUT_DIR}/${binary}")
23 endforeach()
24
25 -set(LINUX_BINARIES init;initrd.img)
25 +set(LINUX_BINARIES init)
26 foreach(binary ${LINUX_BINARIES})
27 list(APPEND BINARIES_DEPENDENCIES "${BIN}/${binary}")
28 endforeach()
@@ -52,7 +52,7 @@ add_custom_command(
52
53 add_custom_target(msipackage DEPENDS ${OUTPUT_PACKAGE})
54 set_target_properties(msipackage PROPERTIES EXCLUDE_FROM_ALL FALSE SOURCES ${PACKAGE_WIX_IN})
55 -add_dependencies(msipackage wsl wslg wslservice wslhost wslrelay wslserviceproxystub init initramfs wslinstall msixgluepackage)
55 +add_dependencies(msipackage wsl wslg wslservice wslhost wslrelay wslserviceproxystub init wslinstall msixgluepackage)
56
57 if (WSL_BUILD_WSL_SETTINGS)
58 add_dependencies(msipackage wslsettings libwsl)
msipackage/package.wix.in
+24 -2
@@ -261,7 +261,6 @@
261 <DirectoryRef Id="TOOLSFOLDER">
262 <Component Id="tools" Guid="F0C8D6BA-1502-41E7-BF72-D93DFA134733" Bitness="always64" UninstallWhenSuperseded="yes">
263 <File Id="init" Source="${BIN}/init" Checksum="yes" />
264 - <File Id="initrd.img" Source="${BIN}/initrd.img"/>
264 <File Id="bsdtar" Source="${BSDTARD_SOURCE_DIR}/${TARGET_PLATFORM}/bsdtar"/>
265
266 <?if "${WSL_DEV_BINARY_PATH}" = "" ?>
@@ -461,7 +460,23 @@
460 Return="check"
461 Execute="deferred"
462 />
464 -
463 +
464 + <CustomAction Id="CreateInitrd"
465 + Impersonate="no"
466 + BinaryRef="wslinstall.dll"
467 + DllEntry="CreateInitrd"
468 + Return="check"
469 + Execute="deferred"
470 + />
471 +
472 + <CustomAction Id="RemoveInitrd"
473 + Impersonate="no"
474 + BinaryRef="wslinstall.dll"
475 + DllEntry="RemoveInitrd"
476 + Return="check"
477 + Execute="deferred"
478 + />
479 +
480 <CustomAction Id="RemoveRegistryKeyProtections"
481 Impersonate="no"
482 BinaryRef="wslinstall.dll"
@@ -525,10 +540,17 @@
540 <!-- This needs to run after the registry is written because this action needs to read the install path -->
541 <Custom Action="RegisterLspCategories" After="WriteRegistryValues" Condition='((not REMOVE~="ALL") or WIX_UPGRADE_DETECTED) and (not UPGRADINGPRODUCTCODE) and (not SKIPLSP = 1)' />
542
543 + <!-- Generate initrd.img from the init binary during installation.
544 + Only run when the tools component is being installed ($tools=3), not during same-version reinstalls -->
545 + <Custom Action="CreateInitrd" After="RegisterLspCategories" Condition='$tools=3' />
546 +
547 <!-- This needs to run before the registry is cleared because this action needs to read the install path.
548 This actions runs on uninstallation and upgrade. -->
549 <Custom Action="UnregisterLspCategories" Before="RemoveRegistryValues" Condition='(REMOVE~="ALL") and (not UPGRADINGPRODUCTCODE) and (not SKIPLSP = 1)' />
550
551 + <!-- Remove initrd.img during uninstallation. Must run after StopServices so the file isn't locked -->
552 + <Custom Action="RemoveInitrd" Before="UnregisterLspCategories" Condition='(REMOVE~="ALL") and (not UPGRADINGPRODUCTCODE)' />
553 +
554 <?if "${WSL_BUILD_WSL_SETTINGS}" = "true" ?>
555 <!-- This needs to run before the registry is written because this action calculates registry key names and values -->
556 <Custom Action="CalculateWslSettingsProtocolIds" Before="WriteRegistryValues" Condition='(not REMOVE~="ALL")' />
packages.config
-1
@@ -24,7 +24,6 @@
24 <package id="Microsoft.WSL.TestDistro" version="2.5.7-47" />
25 <package id="Microsoft.WSLg" version="1.0.73" />
26 <package id="Microsoft.Xaml.Behaviors.WinUI.Managed" version="3.0.0" />
27 - <package id="StrawberryPerl" version="5.32.1.1" />
27 <package id="vswhere" version="3.1.7" />
28 <package id="WinUIEx" version="2.9.0" />
29 <package id="Wix" version="5.0.2" />
src/linux/init/CMakeLists.txt
-17
@@ -50,21 +50,4 @@ set(INIT_LIBRARIES ${COMMON_LINUX_LINK_LIBRARIES} netlinkutil plan9 mountutil co
50 add_linux_executable(init "${SOURCES}" "${HEADERS}" "${INIT_LIBRARIES}")
51 add_dependencies(init localization)
52
53 -set(INITRAMFS ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/${CMAKE_BUILD_TYPE}/initrd.img)
54 -set(INIT ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/${CMAKE_BUILD_TYPE}/init)
55 -
56 -# The initramfs depends on both the init target and binary so it doesn't start too early
57 -# (before the symbols are stripped from the init binary)
58 -# but so that it is still rebuilt if the file itself changes.
59 -add_custom_command(
60 - OUTPUT ${INITRAMFS} "${CMAKE_CURRENT_BINARY_DIR}/CmakeFiles/initramfs"
61 - DEPENDS init ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/${CMAKE_BUILD_TYPE}/init
62 - COMMAND ${PERL_SOURCE_DIR}/bin/perl.exe ${BIN2CPIO} ${INIT} ${INITRAMFS}
63 - COMMAND ${CMAKE_COMMAND} -E touch "${CMAKE_CURRENT_BINARY_DIR}/CmakeFiles/initramfs"
64 - VERBATIM
65 - )
66 -
67 -add_custom_target(initramfs DEPENDS ${INITRAMFS})
68 -
53 set_target_properties(init PROPERTIES FOLDER linux)
70 -set_target_properties(initramfs PROPERTIES FOLDER linux)
src/linux/init/main.cpp
+6
@@ -3985,6 +3985,12 @@ int main(int Argc, char* Argv[])
3985 g_LogFd = 3;
3986 }
3987
3988 + //
3989 + // Log the WSL version to kmesg.
3990 + //
3991 +
3992 + LOG_INFO("WSL version {}", WSL_PACKAGE_VERSION);
3993 +
3994 //
3995 // Ensure /dev/console is present and set as the controlling terminal.
3996 // If opening /dev/console times out, stdout and stderr to the logging file descriptor.
src/windows/common/filesystem.cpp
+110
@@ -1115,6 +1115,116 @@ void wsl::windows::common::filesystem::UpdateInit(_In_ PCWSTR BasePath, _In_ ULO
1115 CopyFileWithMetadata(source.c_str(), dest.c_str(), (LX_S_IFREG | 0755), DistroVersion);
1116 }
1117
1118 +void wsl::windows::common::filesystem::CreateCpioInitrd(_In_ const std::filesystem::path& SourcePath, _In_ const std::filesystem::path& DestPath)
1119 +{
1120 + // Open the source init binary
1121 + wil::unique_hfile sourceFile{
1122 + CreateFileW(SourcePath.c_str(), GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr)};
1123 + THROW_LAST_ERROR_IF(!sourceFile);
1124 +
1125 + // Get file size
1126 + LARGE_INTEGER fileSize{};
1127 + THROW_IF_WIN32_BOOL_FALSE(GetFileSizeEx(sourceFile.get(), &fileSize));
1128 + THROW_HR_IF(E_INVALIDARG, fileSize.HighPart != 0); // File too large
1129 +
1130 + const DWORD initSize = fileSize.LowPart;
1131 + const auto initDataPadding = (4 - (initSize % 4)) % 4;
1132 + wil::unique_hfile destFile{CreateFileW(DestPath.c_str(), GENERIC_WRITE, 0, nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr)};
1133 + THROW_LAST_ERROR_IF(!destFile);
1134 +
1135 + // Clean up the destination file on failure.
1136 + auto deleteFile = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&] {
1137 + destFile.reset();
1138 + LOG_IF_WIN32_BOOL_FALSE(DeleteFileW(DestPath.c_str()));
1139 + });
1140 +
1141 + auto writeCpioHeader = [&destFile](DWORD fileSize, PCSTR name) {
1142 + // CPIO newc header: magic(6) + 13 fields of 8 hex chars each = 110 bytes
1143 + constexpr size_t headerSize = 110;
1144 + const auto nameLen = strlen(name) + 1;
1145 + const auto headerPadding = (4 - ((headerSize + nameLen) % 4)) % 4;
1146 +
1147 + // Get current time for mtime. CPIO newc format only supports 32-bit fields.
1148 + const auto mtime = static_cast<DWORD>(time(nullptr));
1149 +
1150 + char header[headerSize + 1];
1151 + sprintf_s(
1152 + header,
1153 + "070701" // magic
1154 + "%08X" // inode
1155 + "%08X" // mode
1156 + "%08X" // uid
1157 + "%08X" // gid
1158 + "%08X" // nlink
1159 + "%08X" // mtime
1160 + "%08X" // filesize
1161 + "%08X" // devmajor
1162 + "%08X" // devminor
1163 + "%08X" // rdevmajor
1164 + "%08X" // rdevminor
1165 + "%08X" // namesize
1166 + "%08X", // check
1167 + 0,
1168 + (fileSize > 0) ? 0100755 : 0,
1169 + 0,
1170 + 0,
1171 + (fileSize > 0) ? 1 : 0,
1172 + mtime,
1173 + fileSize,
1174 + 0,
1175 + 0,
1176 + 0,
1177 + 0,
1178 + static_cast<DWORD>(nameLen),
1179 + 0);
1180 +
1181 + DWORD bytesWritten;
1182 + THROW_IF_WIN32_BOOL_FALSE(WriteFile(destFile.get(), header, headerSize, &bytesWritten, nullptr));
1183 +
1184 + // Write name with null terminator
1185 + THROW_IF_WIN32_BOOL_FALSE(WriteFile(destFile.get(), name, static_cast<DWORD>(nameLen), &bytesWritten, nullptr));
1186 +
1187 + // Write padding to align to 4 bytes
1188 + if (headerPadding > 0)
1189 + {
1190 + char padding[4] = {0};
1191 + THROW_IF_WIN32_BOOL_FALSE(WriteFile(destFile.get(), padding, static_cast<DWORD>(headerPadding), &bytesWritten, nullptr));
1192 + }
1193 + };
1194 +
1195 + // Write init file header
1196 + writeCpioHeader(initSize, SourcePath.filename().string().c_str());
1197 +
1198 + // Copy file contents
1199 + wsl::windows::common::relay::InterruptableRelay(sourceFile.get(), destFile.get(), nullptr, 64 * 1024);
1200 +
1201 + // Write data padding
1202 + if (initDataPadding > 0)
1203 + {
1204 + char padding[4] = {0};
1205 + DWORD bytesWritten;
1206 + THROW_IF_WIN32_BOOL_FALSE(WriteFile(destFile.get(), padding, static_cast<DWORD>(initDataPadding), &bytesWritten, nullptr));
1207 + }
1208 +
1209 + // Write trailer entry (empty file with name "TRAILER!!!")
1210 + writeCpioHeader(0, "TRAILER!!!");
1211 +
1212 + // Pad the archive to 512-byte boundary
1213 + constexpr DWORD archiveBlockSize = 512;
1214 + LARGE_INTEGER currentPos{};
1215 + THROW_IF_WIN32_BOOL_FALSE(SetFilePointerEx(destFile.get(), {}, &currentPos, FILE_CURRENT));
1216 +
1217 + const auto archivePadding = (archiveBlockSize - (currentPos.LowPart % archiveBlockSize)) % archiveBlockSize;
1218 + if (archivePadding > 0)
1219 + {
1220 + char paddingBuffer[archiveBlockSize] = {0};
1221 + DWORD bytesWritten;
1222 + THROW_IF_WIN32_BOOL_FALSE(WriteFile(destFile.get(), paddingBuffer, archivePadding, &bytesWritten, nullptr));
1223 + }
1224 +
1225 + deleteFile.release();
1226 +}
1227 +
1228 wil::unique_hfile wsl::windows::common::filesystem::WipeAndOpenDirectory(_In_ LPCWSTR pPath)
1229 {
1230 const auto result = wil::RemoveDirectoryRecursiveNoThrow(pPath);
src/windows/common/filesystem.hpp
+5
@@ -228,6 +228,11 @@ std::wstring UnquotePath(_In_ LPCWSTR Path);
228 /// </summary>
229 void UpdateInit(_In_ PCWSTR BasePath, _In_ ULONG DistroVersion);
230
231 +/// <summary>
232 +/// Creates a CPIO newc format initrd archive from a single source file.
233 +/// </summary>
234 +void CreateCpioInitrd(_In_ const std::filesystem::path& SourcePath, _In_ const std::filesystem::path& DestPath);
235 +
236 /// <summary>
237 /// Wipes out the directory with the given path if it exists, then creates it again and returns
238 /// an open directory handle onto it.
src/windows/service/exe/WslCoreVm.cpp
+1 -1
@@ -166,7 +166,7 @@ void WslCoreVm::Initialize(const GUID& VmId, const wil::shared_handle& UserToken
166 // Set the install path of the package.
167 m_installPath = wsl::windows::common::wslutil::GetBasePath();
168
169 - // Initialize the path to the tools folder.
169 + // Initialize the path to the tools folder which also serves as the default rootfs path.
170 m_rootFsPath = m_installPath / LXSS_TOOLS_DIRECTORY;
171
172 // Store the path of the user profile.
src/windows/wslinstall/DllMain.cpp
+53 -15
@@ -42,6 +42,12 @@ static constexpr auto c_wslSettingsProgIDPropertyName = L"WSLSETTINGSPROGID";
42 return NOERROR; \
43 }
44
45 +#define WSL_INSTALL_LOG(Name, ...) \
46 + { \
47 + WSL_LOG(Name, __VA_ARGS__); \
48 + WriteInstallLog(std::format("MSI install: {}", Name)); \
49 + }
50 +
51 #ifndef WSL_OFFICIAL_BUILD
52 void TrustPackageCertificate(LPCWSTR Path)
53 {
@@ -519,8 +525,7 @@ extern "C" UINT __stdcall CleanMsixState(MSIHANDLE install)
525 extern "C" UINT __stdcall DeprovisionMsix(MSIHANDLE install)
526 try
527 {
522 - WSL_LOG("DeprovisionMsix");
523 - WriteInstallLog("MSI install: DeprovisionMsix");
528 + WSL_INSTALL_LOG("DeprovisionMsix");
529
530 const winrt::Windows::Management::Deployment::PackageManager packageManager;
531 const auto result = packageManager.DeprovisionPackageForAllUsersAsync(wsl::windows::common::wslutil::c_msixPackageFamilyName).get();
@@ -543,8 +548,7 @@ catch (...)
548 extern "C" UINT __stdcall RemoveMsixAsSystem(MSIHANDLE install)
549 try
550 {
546 - WSL_LOG("RemoveMsixAsSystem");
547 - WriteInstallLog("MSI install: RemoveMsixAsSystem");
551 + WSL_INSTALL_LOG("RemoveMsixAsSystem");
552
553 const winrt::Windows::Management::Deployment::PackageManager packageManager;
554
@@ -573,8 +577,7 @@ catch (...)
577 extern "C" UINT __stdcall RemoveMsixAsUser(MSIHANDLE install)
578 try
579 {
576 - WSL_LOG("RemoveMsixAsUser");
577 - WriteInstallLog("MSI install: RemoveMsixAsUser");
580 + WSL_INSTALL_LOG("RemoveMsixAsUser");
581
582 const winrt::Windows::Management::Deployment::PackageManager packageManager;
583
@@ -643,8 +646,7 @@ wsl::windows::common::filesystem::TempFile ExtractMsix(MSIHANDLE install)
646 extern "C" UINT __stdcall InstallMsixAsUser(MSIHANDLE install)
647 try
648 {
646 - WSL_LOG("InstallMsixAsUser");
647 - WriteInstallLog("MSI install: InstallMsixAsUser");
649 + WSL_INSTALL_LOG("InstallMsixAsUser");
650
651 // RegisterPackageByFamilyNameAsync() cannot be run as SYSTEM.
652 // If this thread runs as SYSTEM, simply skip this step.
@@ -687,8 +689,7 @@ try
689 // Release a file handle to the MSIX file so that it can be installed.
690 msixFile.Handle.reset();
691
690 - WSL_LOG("InstallMsix", TraceLoggingValue(msixFile.Path.c_str(), "Path"));
691 - WriteInstallLog("MSI install: InstallMsix");
692 + WSL_INSTALL_LOG("InstallMsix", TraceLoggingValue(msixFile.Path.c_str(), "Path"));
693
694 winrt::Windows::Management::Deployment::PackageManager packageManager;
695
@@ -790,8 +791,7 @@ extern "C" UINT __stdcall WslFinalizeInstallation(MSIHANDLE install)
791 {
792 try
793 {
793 - WSL_LOG("WslFinalizeInstallation");
794 - WriteInstallLog(std::format("MSI install: WslFinalizeInstallation"));
794 + WSL_INSTALL_LOG("WslFinalizeInstallation");
795 }
796 CATCH_LOG();
797
@@ -801,9 +801,7 @@ extern "C" UINT __stdcall WslFinalizeInstallation(MSIHANDLE install)
801 extern "C" UINT __stdcall WslValidateInstallation(MSIHANDLE install)
802 try
803 {
804 - WSL_LOG("WslValidateInstallation");
805 -
806 - WriteInstallLog(std::format("MSI install: WslValidateInstallation"));
804 + WSL_INSTALL_LOG("WslValidateInstallation");
805
806 // TODO: Use a more precise version check so we don't install if the Windows build doesn't support lifted.
807
@@ -874,6 +872,46 @@ extern "C" UINT __stdcall UnregisterLspCategories(MSIHANDLE install)
872 return NOERROR;
873 }
874
875 +extern "C" UINT __stdcall CreateInitrd(MSIHANDLE install)
876 +try
877 +{
878 + WSL_INSTALL_LOG("CreateInitrd");
879 +
880 + const auto installRoot = wsl::windows::common::wslutil::GetMsiPackagePath();
881 + THROW_HR_IF(E_INVALIDARG, !installRoot.has_value());
882 +
883 + const auto toolsPath = std::filesystem::path(installRoot.value()) / LXSS_TOOLS_DIRECTORY;
884 + const auto initPath = toolsPath / L"init";
885 + const auto initrdPath = toolsPath / LXSS_VM_MODE_INITRD_NAME;
886 + wsl::windows::common::filesystem::CreateCpioInitrd(initPath, initrdPath);
887 +
888 + return NOERROR;
889 +}
890 +catch (...)
891 +{
892 + LOG_CAUGHT_EXCEPTION();
893 +
894 + return ERROR_INSTALL_FAILURE;
895 +}
896 +
897 +extern "C" UINT __stdcall RemoveInitrd(MSIHANDLE install)
898 +{
899 + try
900 + {
901 + WSL_INSTALL_LOG("RemoveInitrd");
902 +
903 + const auto installRoot = wsl::windows::common::wslutil::GetMsiPackagePath();
904 + THROW_HR_IF(E_INVALIDARG, !installRoot.has_value());
905 +
906 + const auto initrdPath = std::filesystem::path(installRoot.value()) / LXSS_TOOLS_DIRECTORY / LXSS_VM_MODE_INITRD_NAME;
907 + THROW_IF_WIN32_BOOL_FALSE(DeleteFileW(initrdPath.c_str()));
908 + }
909 + CATCH_LOG()
910 +
911 + // Failures in this method aren't fatal.
912 + return NOERROR;
913 +}
914 +
915 std::wstring GetWslSettingsInstalledExePath(MSIHANDLE install)
916 {
917 const auto wslSettingsInstallFolder = GetMsiProperty(install, c_wslSettingsInstalledDirectoryPropertyName);
src/windows/wslinstall/wslinstall.def
+2
@@ -4,6 +4,8 @@ EXPORTS
4 CleanExplorerState
5 CleanMsixState
6 DeprovisionMsix
7 + CreateInitrd
8 + RemoveInitrd
9 WslValidateInstallation
10 WslFinalizeInstallation
11 InstallMsix
test/windows/InstallerTests.cpp
+54 -3
@@ -285,9 +285,9 @@ class InstallerTests
285 }
286 }
287
288 - void ValidatePackageInstalledProperly()
288 + void ValidatePackageInstalledProperly(LPCWSTR expectedVersion = WIDEN(WSL_PACKAGE_VERSION))
289 {
290 - ValidateInstalledVersion(WIDEN(WSL_PACKAGE_VERSION));
290 + ValidateInstalledVersion(expectedVersion);
291
292 auto checkInstalled = []() {
293 auto commandLine = LxssGenerateWslCommandLine(L"echo OK");
@@ -322,7 +322,7 @@ class InstallerTests
322 const auto key = wsl::windows::common::registry::OpenLxssMachineKey();
323 const auto version = wsl::windows::common::registry::ReadString(key.get(), L"MSI", L"Version", L"");
324
325 - VERIFY_ARE_EQUAL(version, WIDEN(WSL_PACKAGE_VERSION));
325 + VERIFY_ARE_EQUAL(version, expectedVersion);
326 VERIFY_IS_FALSE(GetMsiProductCode().empty());
327
328 // TODO: check wslsupport, wslapi and p9rdr
@@ -485,6 +485,57 @@ class InstallerTests
485 ValidatePackageInstalledProperly();
486 }
487
488 + TEST_METHOD(InitrdImgLifecycle)
489 + {
490 + // initrd.img is generated during installation from the init binary.
491 + // It should:
492 + // 1. Exist after a fresh install (generated by custom action)
493 + // 2. Be re-created when the component is upgraded
494 + // 3. Persist when reinstalling the same version
495 + // 4. Be removed on uninstall
496 +
497 + auto initrdPath = m_installedPath / L"tools" / L"initrd.img";
498 +
499 + // Uninstall and install an older version to get a clean state
500 + UninstallMsi();
501 + VERIFY_IS_FALSE(IsMsiPackageInstalled());
502 +
503 + // Case 1: Install older version (2.5.10 which ships initrd.img in the MSI)
504 + InstallGitHubRelease(L"2.5.10");
505 + VERIFY_IS_TRUE(IsMsiPackageInstalled());
506 + VERIFY_IS_TRUE(std::filesystem::exists(initrdPath), L"initrd.img should exist after installing 2.5.10");
507 + ValidatePackageInstalledProperly(L"2.5.10.0");
508 +
509 + // Case 2: Upgrade to current version - initrd.img should be regenerated by custom action
510 + InstallMsi();
511 + VERIFY_IS_TRUE(IsMsiPackageInstalled());
512 + VERIFY_IS_TRUE(std::filesystem::exists(initrdPath), L"initrd.img should exist after upgrade (generated by installer)");
513 + ValidatePackageInstalledProperly();
514 +
515 + // Case 3: Reinstall same version - initrd.img should persist
516 + InstallMsi();
517 + VERIFY_IS_TRUE(IsMsiPackageInstalled());
518 + VERIFY_IS_TRUE(std::filesystem::exists(initrdPath), L"initrd.img should exist after reinstalling same version");
519 + ValidatePackageInstalledProperly();
520 +
521 + // Case 4: Downgrade to older version
522 + InstallGitHubRelease(L"2.5.10");
523 + VERIFY_IS_TRUE(IsMsiPackageInstalled());
524 + VERIFY_IS_TRUE(std::filesystem::exists(initrdPath), L"initrd.img should exist after downgrade");
525 + ValidatePackageInstalledProperly(L"2.5.10.0");
526 +
527 + // Case 5: Uninstall - initrd.img should be removed
528 + UninstallMsi();
529 + VERIFY_IS_FALSE(IsMsiPackageInstalled());
530 + VERIFY_IS_FALSE(std::filesystem::exists(initrdPath), L"initrd.img should be removed on uninstall");
531 +
532 + // Reinstall to leave the system in a consistent state for other tests
533 + InstallMsi();
534 + VERIFY_IS_TRUE(IsMsiPackageInstalled());
535 + VERIFY_IS_TRUE(std::filesystem::exists(initrdPath), L"initrd.img should exist after fresh install");
536 + ValidatePackageInstalledProperly();
537 + }
538 +
539 TEST_METHOD(UpgradeInstallsTheMsiPackage)
540 {
541 // Remove the MSIX package
test/windows/SimpleTests.cpp
+55
@@ -296,5 +296,60 @@ class SimpleTests
296 VERIFY_IS_TRUE(output.find(L"/mnt/c/Program Files (x86)/Common Files") != std::wstring::npos);
297 VERIFY_IS_TRUE(output.find(L"/mnt/c/Users/Test User/AppData/Local/Programs/Microsoft VS Code/bin") != std::wstring::npos);
298 }
299 +
300 + TEST_METHOD(CreateCpioInitrd)
301 + {
302 + using wsl::windows::common::filesystem::CreateCpioInitrd;
303 + using wsl::windows::common::filesystem::TempFile;
304 + using wsl::windows::common::filesystem::TempFileFlags;
305 +
306 + auto validateCpio = [](size_t sourceSize) {
307 + // Create source file with specified size
308 + TempFile sourceFile(GENERIC_WRITE, 0, CREATE_ALWAYS, TempFileFlags::None);
309 + std::vector<char> sourceData(sourceSize, 'X');
310 + DWORD written;
311 + THROW_IF_WIN32_BOOL_FALSE(
312 + WriteFile(sourceFile.Handle.get(), sourceData.data(), static_cast<DWORD>(sourceData.size()), &written, nullptr));
313 + sourceFile.Handle.reset();
314 +
315 + // Create CPIO archive
316 + TempFile destFile(0, 0, CREATE_ALWAYS, TempFileFlags::None, L"img");
317 + CreateCpioInitrd(sourceFile.Path, destFile.Path);
318 +
319 + // Read and validate the CPIO archive
320 + wil::unique_hfile cpioHandle{CreateFileW(
321 + destFile.Path.c_str(), GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr)};
322 + THROW_LAST_ERROR_IF(!cpioHandle);
323 +
324 + LARGE_INTEGER cpioSize{};
325 + THROW_IF_WIN32_BOOL_FALSE(GetFileSizeEx(cpioHandle.get(), &cpioSize));
326 + VERIFY_ARE_EQUAL(cpioSize.QuadPart % 512, 0LL); // Archive padded to 512-byte boundary
327 +
328 + char header[110] = {};
329 + DWORD bytesRead;
330 + THROW_IF_WIN32_BOOL_FALSE(ReadFile(cpioHandle.get(), header, sizeof(header), &bytesRead, nullptr));
331 + VERIFY_ARE_EQUAL(bytesRead, static_cast<DWORD>(sizeof(header)));
332 +
333 + // Parse CPIO newc header: magic(6) ino mode uid gid nlink mtime filesize devmajor devminor rdevmajor rdevminor namesize check
334 + DWORD fileSize, nameSize;
335 + VERIFY_ARE_EQUAL(sscanf_s(header, "070701%*8x%*8x%*8x%*8x%*8x%*8x%8x%*8x%*8x%*8x%*8x%8x", &fileSize, &nameSize), 2);
336 +
337 + // Verify filename matches source file
338 + auto expectedName = sourceFile.Path.filename().string();
339 + VERIFY_ARE_EQUAL(nameSize, static_cast<DWORD>(expectedName.size() + 1));
340 +
341 + std::string filename(nameSize, '\0');
342 + THROW_IF_WIN32_BOOL_FALSE(ReadFile(cpioHandle.get(), filename.data(), nameSize, &bytesRead, nullptr));
343 + VERIFY_ARE_EQUAL(filename.c_str(), expectedName);
344 +
345 + VERIFY_ARE_EQUAL(fileSize, static_cast<DWORD>(sourceSize));
346 + };
347 +
348 + // Test various sizes to exercise 4-byte alignment padding
349 + for (size_t size : {0, 1, 2, 3, 4, 5, 100, 1024, 4096, 65536})
350 + {
351 + validateCpio(size);
352 + }
353 + }
354 };
355 } // namespace SimpleTests
\ No newline at end of file
tools/bin2cpio/CPIOImage.pm deleted
-236
@@ -1,236 +0,0 @@
1 -#
2 -# Module to create CPIO packed strings.
3 -#
4 -# N.B. The library currently only supports minimal operations in the "New ASCII
5 -# Format" including:
6 -# +Adding files
7 -# +Adding trailer
8 -#
9 -
10 -package CPIOImage;
11 -
12 -use strict;
13 -use Math::BigInt qw/:constant/;
14 -use Exporter 'import';
15 -use File::Basename;
16 -use bytes;
17 -no bytes;
18 -
19 -our @EXPORT = qw(AddObject,
20 - AddTrailer,
21 - WriteFile
22 - );
23 -
24 -#
25 -# CPIO constants
26 -#
27 -use constant MAGIC_NEWC => "070701";
28 -use constant HEADER_ENTRY_FORMAT => "%08X";
29 -use constant CHECK => 0;
30 -use constant NAME_EXTRA_PADDING => 2;
31 -use constant HEADER_PADDING_SIZE => 4;
32 -use constant FILE_PADDING_SIZE => 512;
33 -
34 -use constant TRAILER => "TRAILER!!!";
35 -
36 -#
37 -# CPIO values assumed for now.
38 -#
39 -# N.B. These need to be dynamically generated if used outside of basic scenarios
40 -# like initramfs.
41 -#
42 -use constant INODE => 0;
43 -use constant MODE => oct("100755");
44 -use constant UID => 0;
45 -use constant GID => 0;
46 -use constant NLINK => 0;
47 -use constant DEVMAJOR => 0;
48 -use constant DEVMINOR => 0;
49 -use constant RDEVMAJOR => 0;
50 -use constant RDEVMINOR => 0;
51 -
52 -sub ErrorExit($$) {
53 - my $line_number = shift;
54 - my $error_message = shift;
55 -
56 - print STDERR "$0($line_number, 1): error LX1: $error_message\n";
57 - exit 255;
58 -}
59 -
60 -#
61 -# Routine Description:
62 -#
63 -# This routine computes padding.
64 -#
65 -# Arguments:
66 -#
67 -# size - Supplies the size to align.
68 -#
69 -# alignment - Supplies the alignment
70 -#
71 -# Return Value:
72 -#
73 -# The number of bytes to pad.
74 -#
75 -sub GetPadding($$) {
76 - my $Size = shift;
77 - my $Alignment = shift;
78 -
79 - my $Result = $Size % $Alignment;
80 - if ($Result != 0)
81 - {
82 - $Result = $Alignment - $Result;
83 - }
84 -
85 - return $Result;
86 -}
87 -
88 -#
89 -# Routine Description:
90 -#
91 -# This routine creates a CPIO entry.
92 -#
93 -# Arguments:
94 -#
95 -# name - Supplies the name.
96 -#
97 -# fileContentsRef - Supplies the file contents reference.
98 -#
99 -# fileSize - Supplies the file size.
100 -#
101 -# Return Value:
102 -#
103 -# None.
104 -#
105 -sub CreateEntry($$$) {
106 - my $name = shift;
107 - my $fileContentsRef = shift;
108 - my $fileSize = shift;
109 -
110 - # Write static values.
111 - my $cpioEntry = MAGIC_NEWC;
112 - $cpioEntry .= sprintf(HEADER_ENTRY_FORMAT, INODE);
113 - my $mode = sprintf(HEADER_ENTRY_FORMAT, MODE);
114 - $cpioEntry .= sprintf(HEADER_ENTRY_FORMAT, MODE);
115 - $cpioEntry .= sprintf(HEADER_ENTRY_FORMAT, UID);
116 - $cpioEntry .= sprintf(HEADER_ENTRY_FORMAT, GID);
117 - $cpioEntry .= sprintf(HEADER_ENTRY_FORMAT, NLINK);
118 - $cpioEntry .= sprintf(HEADER_ENTRY_FORMAT, time());
119 -
120 - # Write the file size
121 - $cpioEntry .= sprintf(HEADER_ENTRY_FORMAT, $fileSize);
122 -
123 - # Write static values.
124 - $cpioEntry .= sprintf(HEADER_ENTRY_FORMAT, DEVMAJOR);
125 - $cpioEntry .= sprintf(HEADER_ENTRY_FORMAT, DEVMINOR);
126 - $cpioEntry .= sprintf(HEADER_ENTRY_FORMAT, RDEVMAJOR);
127 - $cpioEntry .= sprintf(HEADER_ENTRY_FORMAT, RDEVMINOR);
128 -
129 - # Write the name size including the NULL terminator.
130 - my $nameSize = length($name) + 1;
131 - $cpioEntry .= sprintf(HEADER_ENTRY_FORMAT, $nameSize);
132 -
133 - # Write static value.
134 - $cpioEntry .= sprintf(HEADER_ENTRY_FORMAT, CHECK);
135 -
136 - # Write the name including the NULL terminator, padded out to a multiple of 4
137 - $cpioEntry .= $name."\0";
138 - $cpioEntry .= "\0" x (GetPadding($nameSize + NAME_EXTRA_PADDING, HEADER_PADDING_SIZE));
139 -
140 - # Write the contents, padded out to a multiple of 4
141 - $cpioEntry .= ${$fileContentsRef};
142 - $cpioEntry .= "\0" x GetPadding($fileSize, HEADER_PADDING_SIZE);
143 -
144 - return $cpioEntry;
145 -}
146 -
147 -#
148 -# Routine Description:
149 -#
150 -# This routine adds an object to the cpio image stream.
151 -#
152 -# N.B. Only files are currently supported.
153 -#
154 -# Arguments:
155 -#
156 -# cpioImage - Supplies the cpio image.
157 -#
158 -# path - Supplies the path of the object to add.
159 -#
160 -# Return Value:
161 -#
162 -# None
163 -#
164 -sub AddObject($$$) {
165 - my $cpioImageRef = shift;
166 - my $path = shift;
167 - my $name = shift;
168 -
169 - # Open and read the file
170 - my $INPUT_HANDLE;
171 - unless(open($INPUT_HANDLE, "<", $path)){ErrorExit(__LINE__, $!);}
172 - binmode($INPUT_HANDLE);
173 - local $/ = undef;
174 - my $fileContents = <$INPUT_HANDLE>;
175 - close($INPUT_HANDLE);
176 -
177 - # Check the size of the file
178 - my $fileSize;
179 - unless(defined($fileSize = -s $path)){ErrorExit(__LINE__, "Could not check file size at line $.");}
180 - my $fileContentsSize = bytes::length($fileContents);
181 - if ($fileSize != $fileContentsSize){ErrorExit(__LINE__, "Invalid file size at line $.");}
182 -
183 - # Write the entry
184 - ${$cpioImageRef} .= CreateEntry($name, \$fileContents, $fileSize);
185 -}
186 -
187 -#
188 -# Routine Description:
189 -#
190 -# This routine adds the trailer to the cpio image stream.
191 -#
192 -# Arguments:
193 -#
194 -# cpioImageRef - Supplies the cpio image reference.
195 -#
196 -# Return Value:
197 -#
198 -# None
199 -#
200 -sub AddTrailer($) {
201 - my $cpioImageRef = shift;
202 -
203 - # Write the entry
204 - my $fileContents = "";
205 - ${$cpioImageRef} .= CreateEntry(TRAILER, \$fileContents, 0);
206 -
207 - # Pad out the file to the block size
208 - my $size = bytes::length(${$cpioImageRef});
209 - ${$cpioImageRef} .= "\0" x GetPadding($size, FILE_PADDING_SIZE);
210 -}
211 -
212 -#
213 -# Routine Description:
214 -#
215 -# This routine writes the cpio image stream to a file.
216 -#
217 -# Arguments:
218 -#
219 -# cpioImageRef - Supplies the cpio image reference.
220 -#
221 -# name - Supplies the name of the file.
222 -#
223 -# Return Value:
224 -#
225 -# None
226 -#
227 -sub WriteFile($$) {
228 - my $cpioImageRef = shift;
229 - my $name = shift;
230 -
231 - my $OUTPUT_HANDLE;
232 - unless(open($OUTPUT_HANDLE, ">", $name)){ErrorExit(__LINE__, $!);}
233 - binmode($OUTPUT_HANDLE);
234 - print $OUTPUT_HANDLE ${$cpioImageRef};
235 - close($OUTPUT_HANDLE);
236 -}
\ No newline at end of file
tools/bin2cpio/bin2cpio.pl deleted
-30
@@ -1,30 +0,0 @@
1 -#
2 -# Script to convert a binary to a CPIO image.
3 -#
4 -
5 -use strict;
6 -use File::Basename;
7 -use lib dirname (__FILE__);
8 -use CPIOImage;
9 -
10 -if($#ARGV + 1 != 2)
11 -{
12 - ErrorExit(__LINE__, "Invalid command line at line $.");
13 -}
14 -
15 -my $inputFile = $ARGV[0];
16 -my $outputFile = $ARGV[1];
17 -
18 -my $cpioStream = "";
19 -CPIOImage::AddObject(\$cpioStream, $inputFile, "init");
20 -CPIOImage::AddTrailer(\$cpioStream);
21 -CPIOImage::WriteFile(\$cpioStream, $outputFile);
22 -exit 0;
23 -
24 -sub ErrorExit($$) {
25 - my $line_number = shift;
26 - my $error_message = shift;
27 -
28 - print STDERR "$0($line_number, 1): error LX2: $error_message\n";
29 - exit 255;
30 -}
\ No newline at end of file