Mitme .NET 10 rakenduse paigaldamine ühte kausta ilma DLL-konfliktideta
1. Probleem
.NET 10 rakenduse tavaline raamistikupõhine (framework-dependent) Release-ehitus tekitab kausta hulga faile, mis näivad jagatavatena, kuid enamasti seda ei ole:
AlamkollektsiooniKopeerimineValikuga.deps.json
AlamkollektsiooniKopeerimineValikuga.dll
AlamkollektsiooniKopeerimineValikuga.exe
AlamkollektsiooniKopeerimineValikuga.pdb
AlamkollektsiooniKopeerimineValikuga.runtimeconfig.json
AlamkollektsiooniKopeerimineValikugaLib.dll / .pdb
BouncyCastle.Crypto.dll
Google.Protobuf.dll
K4os.Compression.LZ4.dll
K4os.Compression.LZ4.Streams.dll
K4os.Hash.xxHash.dll
MySql.Data.dll
System.CommandLine.dll
cs\ de\ es\ fr\ it\ ja\ ko\ pl\ pt-BR\ ru\ tr\ zh-Hans\ zh-Hant\ (satelliit-ressursiteegid)
runtimes\ (natiivteegid)
Kui teise rakenduse ehitustulemus paigutada samasse kausta, tekib klassikaline DLL-põrgu: kui kaks rakendust viitavad sama NuGet-paketi eri versioonidele (nt MySql.Data 8.0.33 vs 8.4.x), jääb peale see, kelle failid viimasena kopeeriti. Iga rakenduse deps.json sisaldab täpselt seda versiooni, millega rakendus kompileeriti, mistõttu „kaotanud" rakendus võib käivitumisel anda teegi laadimise vea või – veelgi halvem – töötada vaikselt valesti.
Milleks iga väljundfail on
| Fail | Otstarve | Rakenduste vahel jagatav? |
|---|---|---|
*.deps.json | Täpne sõltuvuste graaf (teegid + versioonid), mida host käivitumisel loeb | Ei – rangelt rakendusepõhine |
*.runtimeconfig.json | Sihtruntime'i versioon ja seaded | Ei – rangelt rakendusepõhine |
*.exe (apphost) | Natiivne käivitaja, mis on kõvasti seotud rakenduse põhi-DLL-iga | Ei – definitsiooni järgi rakendusepõhine |
Rakenduse .dll / .pdb | Rakenduse kompileeritud IL-kood ja silumissümbolid | Ei |
| NuGet-sõltuvuste DLL-id | Kopeeritakse ehitamisel NuGet-vahemälust | Ainult siis, kui kõik rakendused kasutavad täpselt samu paketiversioone |
Keelekaustad (cs, de, …) | Satelliit-ressursiteegid (lokaliseeritud sõnumid, siin MySql.Data omad) | Sama versioonisõltuvuse probleem |
runtimes\ | Platvormispetsiifilised natiivteegid NuGet-pakettidest | Sama versioonisõltuvuse probleem |
.NET runtime ise raamistikupõhise ehituse puhul selles kaustas ei ole – see on paigaldatud masinaüleselt ja on niikuinii jagatud. Kõik väljundkaustas olev on rakenduse enda pagas.
Viide: .NET-rakenduste avaldamise ülevaade – https://learn.microsoft.com/en-us/dotnet/core/deploying/
2. Samm 1 – tarbetute satelliit- (keele-) teekide eemaldamine
Kolmteist keelekausta pärinesid MySql.Data lokaliseeritud veateadetest. Kui ingliskeelsetest sõnumitest piisab, piirab MSBuild-atribuut SatelliteResourceLanguages väljundisse kopeeritavaid satelliitteeke:
<PropertyGroup>
<SatelliteResourceLanguages>en</SatelliteResourceLanguages>
</PropertyGroup>
Praktikas selgunud olulised detailid:
- Atribuut peab olema seatud käivitatava projekti failis. Iga projekt otsustab ise, millised satelliitteegid ta oma NuGet-sõltuvustest väljundisse kopeerib, seega ainult viidatud klassiteegis seadmine käivitatava projekti väljundit ei puhasta. Mõlemas projektis seadmine on ohutu.
- Lahendusülene alternatiiv: paiguta atribuut üks kord lahenduse juurkausta faili
Directory.Build.props– MSBuild rakendab selle automaatselt kõigile allolevatele projektidele:
<Project>
<PropertyGroup>
<SatelliteResourceLanguages>en</SatelliteResourceLanguages>
</PropertyGroup>
</Project>
- Clean ega Rebuild vanu kaustu ei eemalda.
dotnet clean/ VS Clean kustutab ainult faile, mis on kirjas eelmise ehituse jälgimislogis. Kui uus ehitus satelliitteeke enam ei tooda, muutuvad vanad kaustad orbudeks, mida ükski ehitussamm ei „oma". Õnnestumist saab kontrollidadeps.jsonfaili suuruse vähenemisest (antud juhul 7 756 → 6 191 baiti); seejärel kustutabin-kaust käsitsi üks kord. Kaustad enam tagasi ei teki.
Viited:
SatelliteResourceLanguagesja muud SDK-projektide atribuudid – https://learn.microsoft.com/en-us/dotnet/core/project-sdk/msbuild-propsDirectory.Build.propsmehhanism – https://learn.microsoft.com/en-us/visualstudio/msbuild/customize-by-directory
3. Samm 2 – ühefaililine avaldamine (single-file publish)
Ühefaililine avaldamine pakendab rakenduse DLL-i ja kõik hallatavad (managed) NuGet-sõltuvused exe-faili sisse. Hallatavad teegid laaditakse otse pakendist mällu (kettale midagi lahti ei pakita). See kõrvaldab peaaegu kõik jagatud failidest tulenevad konfliktid.
Töötav avaldamisprofiil (Properties\PublishProfiles\FolderProfile.pubxml):
<?xml version="1.0" encoding="utf-8"?>
<Project>
<PropertyGroup>
<Configuration>Release</Configuration>
<Platform>Any CPU</Platform>
<PublishDir>bin\Release\net10.0-windows7.0\publish\win-x64\</PublishDir>
<PublishProtocol>FileSystem</PublishProtocol>
<_TargetId>Folder</_TargetId>
<TargetFramework>net10.0-windows7.0</TargetFramework>
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
<SelfContained>false</SelfContained>
<PublishSingleFile>true</PublishSingleFile>
<PublishReadyToRun>false</PublishReadyToRun>
<IncludeNativeLibrariesForSelfExtract>true</IncludeNativeLibrariesForSelfExtract>
<DebugType>none</DebugType>
<DebugSymbols>false</DebugSymbols>
</PropertyGroup>
</Project>
Sama käsurealt:
dotnet publish -c Release -r win-x64 --self-contained false ^
-p:PublishSingleFile=true ^
-p:IncludeNativeLibrariesForSelfExtract=true ^
-p:DebugType=none -p:DebugSymbols=false
Atribuutide selgitused
PublishSingleFile=true
Pakendab rakenduse ja kõik hallatavad sõltuvused üheks käivitatavaks failiks. Nõuab RuntimeIdentifier-i, sest apphost on platvormispetsiifiline.
Dokumentatsioon: https://learn.microsoft.com/en-us/dotnet/core/deploying/single-file/overview
SelfContained=false (raamistikupõhine)
.NET 10 runtime'i pakendisse ei lisata; see peab olema sihtmasinas paigaldatud. Nii jäi exe suuruseks ~7 MB. Väärtus true pakendaks kogu runtime'i (~70+ MB exe kohta), kuid kaotaks runtime'i paigaldamise nõude.
Dokumentatsioon: https://learn.microsoft.com/en-us/dotnet/core/deploying/
RuntimeIdentifier=win-x64
Valib sihtplatvormi natiivse apphost'i ja natiivsõltuvuste jaoks.
RID-kataloog: https://learn.microsoft.com/en-us/dotnet/core/rid-catalog
IncludeNativeLibrariesForSelfExtract=true
Vaikimisi (alates .NET 5-st) jätab ühefaililine avaldamine natiivsed DLL-id lahtiste failidena exe kõrvale. Selles projektis olid nendeks MySql.Data Kerberos/GSSAPI teegid: comerr64.dll, gssapi64.dll, k5sprt64.dll, krb5_64.dll, krbcc64.dll – viimane allesjäänud konfliktipind. Selle atribuudiga pakitakse natiivteegid exe sisse ja pakitakse käivitumisel lahti rakendusepõhisesse ajutisse kausta, mistõttu erinevad rakendused ei puutu kunagi üksteise natiivfaile. Exe kasvas umbes 1,8 MB võrra.
Dokumentatsioon (sama leht, jaotis „Include native libraries"): https://learn.microsoft.com/en-us/dotnet/core/deploying/single-file/overview
(Lahtipakkimise asukohta saab vajadusel suunata keskkonnamuutujaga DOTNET_BUNDLE_EXTRACT_BASE_DIR – kirjeldatud samal lehel.)
DebugType=none + DebugSymbols=false
Keelab .pdb-failide genereerimise Release/publish puhul täielikult. Tähelepanu: avaldamisprofiil mõjutab ainult käivitatavat projekti; viidatud klassiteek kompileerub oma seadetega ja tekitab endiselt .pdb faili. Töökindel lahendus on seada need atribuudid projektipõhiselt (või üks kord failis Directory.Build.props), tingimusega Release-konfiguratsioonile, et Debug-ehitused säilitaksid täielikud sümbolid:
<PropertyGroup Condition="'$(Configuration)' == 'Release'">
<DebugType>none</DebugType>
<DebugSymbols>false</DebugSymbols>
</PropertyGroup>
Teadmist väärt alternatiiv: DebugType=embedded paigutab sümbolid exe sisse – lahtist .pdb-d pole, kuid stack trace'id säilitavad faili- ja reainfo. Kasulik, kui välidiagnostika on paarisaja kilobaidi kokkuhoiust olulisem.
Dokumentatsioon: https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/compiler-options/code-generation
PublishReadyToRun=false
ReadyToRun kompileerib IL-koodi ette natiivkoodiks, mis kiirendab käivitumist suurema faili hinnaga. Siin välja lülitatud; lülita sisse rakendusepõhiselt, kui käivituskiirus on oluline.
Dokumentatsioon: https://learn.microsoft.com/en-us/dotnet/core/deploying/ready-to-run
Tulemus
Pärast vana publish-kausta ühekordset kustutamist (publish, nagu ka clean, ei eemalda eelmiste konfiguratsioonide orvuks jäänud faile) ja uuesti avaldamist:
AlamkollektsiooniKopeerimineValikuga.exe (~7 MB, üks fail)
Muud midagi. Hallatavad sõltuvused, satelliitteekide käsitlus ja natiivsed Kerberos-teegid on kõik exe sees.
4. Mitme rakenduse paigaldamine ühte kausta
Ülaltoodud seadistusega võib ühte jagatud kausta paigutada ükskõik kui palju rakendusi:
- Iga rakendus on üks unikaalse nimega exe – jagatud faile pole, seega on versioonikonfliktid välistatud.
- Natiivteegid pakitakse käivitumisel lahti rakendusepõhistesse ajutistesse kaustadesse, seega ei teki ka käitusaegseid kokkupõrkeid.
- Iga rakendus võib kasutada MySql.Data või mis tahes muu paketi erinevat versiooni, mõjutamata teisi.
Üks praktiline soovitus: ära suuna mitme projekti PublishDir-i otse samasse jagatud kausta. Publish-samm võib kustutada faile, mida ta peab aegunuks, ja nii võib kaduma minna teise rakenduse exe. Ohutum muster:
avalda iga projekt oma kausta (nagu ülal seadistatud)
│
└──► kopeeri valminud exe-failid ühisesse paigalduskausta
Kopeerimise saab teha väikese skriptiga või MSBuildi publish-järgse sammuna, näiteks:
<Target Name="CopyToDeployFolder" AfterTargets="Publish">
<Copy SourceFiles="$(PublishDir)$(AssemblyName).exe"
DestinationFolder="E:\LenneApps\Deploy\" />
</Target>
5. Ühefaililise paigalduse kompromissid ja tähelepanekud
- Raamistikupõhine ühefaililine exe nõuab endiselt .NET 10 runtime'i igas sihtmasinas. Ettevõttesisese paigalduse puhul, kus runtime'i hallatakse tsentraalselt, on see enamasti õige valik: exe-d jäävad väikeseks ja runtime'i turvapaigad rakenduvad kõigile rakendustele korraga.
- Mõned API-d käituvad ühefaililises rakenduses teisiti.
Assembly.Locationtagastab tühja stringi; kood, mis selle põhjal teid koostab, peab kasutama hoopisAppContext.BaseDirectory-t. Kolmandate osapoolte paketid komistavad selle otsa aeg-ajalt. Täielik ühilduvustabel: https://learn.microsoft.com/en-us/dotnet/core/deploying/single-file/overview (jaotis „API incompatibility"). - Valikuline suuruse vähendamine:
EnableCompressionInSingleFile=truepakib pakendatud teegid kokku väikese käivitusaja hinnaga. Trimmimine (PublishTrimmed=true) on saadaval ainult self-contained rakendustele ja vajab testimist refleksioonirohkete teekidega nagu MySql.Data. - Rusikareegel aegunud failide kohta: kui ehitus- või avaldamisseaded muudavad, mida toodetakse, kustuta vana
bin-/publish-kaust üks kord. Ei Clean ega Publish ei eemalda faile, mida uus konfiguratsioon enam ei genereeri.
6. Ühefaililise avaldamise ajalugu ja Windowsi tugi
Kuidas võimalus arenes
- .NET Core 3.0 (september 2019) –
PublishSingleFileilmus esimest korda. Toona oli see sisuliselt isepakkiv arhiiv: käivitumisel pakiti kõik failid (nii hallatavad kui natiivsed) kettale ajutisse kausta lahti ja käivitati sealt. - .NET 5 (november 2020) – praegune „päris" ühefaililine mudel: hallatavad teegid laaditakse otse exe seest mällu, kettale ei pakita midagi. Samas versioonis tuli ka
IncludeNativeLibrariesForSelfExtract, sest natiivteegid jäeti nüüd vaikimisi pakendist välja (varem pakiti kõik kaasa). Just seda kombinatsiooni käesolev juhend kasutabki. - .NET 6 (november 2021) – lisandus
EnableCompressionInSingleFilepakendatud teekide kokkupakkimiseks ning mudel stabiliseerus tänasel kujul.
Seega on selles dokumendis kirjeldatud tehnika (mällu laadimine + natiivteekide kaasapakkimine) olemas alates .NET 5-st, novembrist 2020.
Millised Windowsi versioonid tulemust jooksutavad
.NET 10 on ametlikult toetatud Windows 11 (23H2, 24H2, 25H2, 26H1) ja Windows 10 peal alates versioonist 1607 (Enterprise/LTSC kanalid: 1607, 1809, 21H2). Praktikas tähendab see Windows 10 (1607+) ja Windows 11. Tavaline Windows 10 22H2 langes ametlikust toest välja koos Windowsi enda toe lõppemisega oktoobris 2025, kuigi rakendused seal endiselt töötavad. Windows 7 ja 8.1 peal .NET 10 ei tööta üldse – nende tugi kadus juba .NET 7/8 ajal.
Märkus TFM-i net10.0-windows7.0 kohta: „7.0" ei tähenda Windows 7 tuge. See on lihtsalt vaikimisi deklareeritav minimaalne Windowsi API versioon, mille MSBuild paneb, kui <TargetFramework>net10.0-windows</TargetFramework> on kirjutatud ilma versioonita. Tegelik miinimum-OS on see, mida runtime ise toetab.
Ajakohane .NET 10 toetatud operatsioonisüsteemide nimekiri: https://github.com/dotnet/core/blob/main/release-notes/10.0/supported-os.md
7. Natiivmaailm: kuidas teha C++ rakendusest üks .exe fail
Natiivrakendustel puudub .NET-i laadne bundle-mehhanism, mida runtime oskaks mälust laadida, seega on lähenemised teistsugused.
Variant 1 – staatiline linkimine (soovitatav)
Sõltuvused lingitakse kompileerimisel .lib staatiliste teekidena otse exe sisse, DLL-e ei tekigi. MSVC puhul tähendab see runtime'i lülitit /MT (/MD asemel) ja kolmandate osapoolte teekide staatilisi variante. vcpkg teeb selle lihtsaks: triplet x64-windows-static ehitab kõik sõltuvused staatilistena. Tulemus on üksainus exe ilma käitusaegse maagiata – kiireim käivitus ja kõige töökindlam variant. Miinused: suurem exe, iga teegi uuendus nõuab uuesti linkimist, ja mõned litsentsid (nt LGPL, sh Qt) seavad staatilisele linkimisele piiranguid.
- MSVC
/MTvs/MD: https://learn.microsoft.com/en-us/cpp/build/reference/md-mt-ld-use-run-time-library - vcpkg tripletid: https://learn.microsoft.com/en-us/vcpkg/users/triplets
Variant 2 – DLL-ide pakkimine valmis exe sisse
Tööriistad nagu Enigma Virtual Box (tasuta) või BoxedApp Packer võtavad valmis exe + DLL-id ja teevad neist ühe faili, mis virtualiseerib failisüsteemi kutsed – DLL-id eksisteerivad ainult mälus. Töötab ka siis, kui lähtekoodi või staatilisi teeke pole (suletud lähtekoodiga DLL-id). Miinused: viirusetõrjed suhtuvad pakitud exe-desse kohati kahtlustavalt ning COM-registreerimist vajavad DLL-id ei pruugi töötada.
- Enigma Virtual Box: https://enigmaprotector.com/en/aboutvb.html
Variant 3 – DLL ressursina ja mälust laadimine
DLL-id paigutatakse exe ressurssidesse ja laaditakse käivitumisel kas ajutisse kausta tavalise LoadLibrary-ga (nagu .NET Core 3.0 omal ajal tegi) või otse mälust MemoryModule teegiga, mis implementeerib oma PE-laadija. Kõige paindlikum, aga ka kõige rohkem käsitööd nõudev tee; MemoryModule'il on piirangud (erandikäsitlus x64-l, mõned DLL-id eeldavad failitee olemasolu).
- MemoryModule: https://github.com/fancycode/MemoryModule
Järeldus
GUI-rakenduste puhul on staatilise linkimisega natiivset exe-d tegelikult harva vaja: .NET raamistiku kasutamine on hästi hallatav, arendus on kiirem ning jõudlus ei jää natiivrakendusele oluliselt alla. Ühefaililine .NET-avaldamine (peatükid 3–4) annab sama paigaldusmugavuse – üks exe, null DLL-konflikti – ilma natiivmaailma linkimis- ja litsentsimuredeta. Natiivne staatiline linkimine tasub end ära peamiselt siis, kui runtime'i paigaldamine sihtmasinasse pole võimalik, rakendus peab olema minimaalse mälujäljega või tegu on niikuinii C++ koodibaasiga (nt riistvaralähedased tööriistad).












