Liigu peamise sisu juurde

Mitme .NET 10 rakenduse paigaldamine ühte kausta ilma DLL-konfliktideta

· 10 min lugemine
Infokiir OÜ

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

FailOtstarveRakenduste vahel jagatav?
*.deps.jsonTäpne sõltuvuste graaf (teegid + versioonid), mida host käivitumisel loebEi – rangelt rakendusepõhine
*.runtimeconfig.jsonSihtruntime'i versioon ja seadedEi – rangelt rakendusepõhine
*.exe (apphost)Natiivne käivitaja, mis on kõvasti seotud rakenduse põhi-DLL-igaEi – definitsiooni järgi rakendusepõhine
Rakenduse .dll / .pdbRakenduse kompileeritud IL-kood ja silumissümbolidEi
NuGet-sõltuvuste DLL-idKopeeritakse ehitamisel NuGet-vahemälustAinult 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-pakettidestSama 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:

  1. 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.
  2. 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>
  1. 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 kontrollida deps.json faili suuruse vähenemisest (antud juhul 7 756 → 6 191 baiti); seejärel kustuta bin-kaust käsitsi üks kord. Kaustad enam tagasi ei teki.

Viited:


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:

  1. Iga rakendus on üks unikaalse nimega exe – jagatud faile pole, seega on versioonikonfliktid välistatud.
  2. Natiivteegid pakitakse käivitumisel lahti rakendusepõhistesse ajutistesse kaustadesse, seega ei teki ka käitusaegseid kokkupõrkeid.
  3. 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.Location tagastab tühja stringi; kood, mis selle põhjal teid koostab, peab kasutama hoopis AppContext.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=true pakib 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)PublishSingleFile ilmus 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 EnableCompressionInSingleFile pakendatud 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.

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.

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).

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).


8. Kiirviited – dokumentatsiooni lingid

TeemaLink
Ühefaililine paigaldus (PublishSingleFile, IncludeNativeLibrariesForSelfExtract, lahtipakkimine, API piirangud)https://learn.microsoft.com/en-us/dotnet/core/deploying/single-file/overview
.NET paigaldusmudelid (raamistikupõhine vs self-contained)https://learn.microsoft.com/en-us/dotnet/core/deploying/
SDK-projektide MSBuild-atribuudid (SatelliteResourceLanguages, publish-atribuudid)https://learn.microsoft.com/en-us/dotnet/core/project-sdk/msbuild-props
Directory.Build.props / ehituse kohandamine kaustapõhiselthttps://learn.microsoft.com/en-us/visualstudio/msbuild/customize-by-directory
DebugType / DebugSymbols kompilaatoriseadedhttps://learn.microsoft.com/en-us/dotnet/csharp/language-reference/compiler-options/code-generation
ReadyToRun kompileeriminehttps://learn.microsoft.com/en-us/dotnet/core/deploying/ready-to-run
Runtime identifier (RID) katalooghttps://learn.microsoft.com/en-us/dotnet/core/rid-catalog
Self-contained rakenduste trimmiminehttps://learn.microsoft.com/en-us/dotnet/core/deploying/trimming/trim-self-contained