nuget without internet survival guide
NuCache V2
Заводим следующие папки:
mkdir -p ~/.nuget/nucache/bin
mkdir -p ~/.nuget/nucache/http
mkdir -p ~/.nuget/nucache/packagesВ bin складываем скрипт, делаем его executable chmod +x ~/.nuget/nucache/bin/nucache и добавляем эту папку в PATH
В http будут жить json кэши ответов api, а в packages соотв сохраненные пакеты
Поддерживаемые команды:
nucache status- показывает текущий режим online/offlinenucache offline- переключает в offline режимnucache online- переключает в online режимnucache sync- перепроверяет все что найдет~/.nuget/packages/**/*.nupkgnucache /path/to/project- выкачивает все что найдет/path/to/project/**/*.csprojnucache Newtonsoft.Json- выкачивает последнюю версию пакета и все его зависимости, алиасnucache add package Newtonsoft.Jsonnucache Newtonsoft.Json 12.0.3- выкачивает конретную версию и зависимости, алиасnucache add package Newtonsoft.json --version 12.0.3
Как с этим работать:
- Always offline вариант - переключаемся в офлайн режим, тем самым вынуждая себя не забывать вызывать nucache, а когда пропадет связь ничего не надо будет делать и все будет работать
- Online/offline - переключаемся туда сюда, пользуемся dotnet add package как обычно, как появиться связь вызываем nucache sync и nucache /path/to/project что бы вытянуть пикеты
Примечание: проверялось на macos, скорее всего будет работать в linux из коробки, для windows скорее всего нужно сделать маленький cmd враппер и добавить в path его, подсмотреть в репозитории кафки
Примечание: все так же остается прикол с пакетами которые не референсятся по типу Microsoft.NETCore.App.Ref или Microsoft.NETCore.App.Host.osx-x64 потому есть смысл пока есть сесть сделать для пары тройки проектов что то типа:
dotnet nuget locals all --clear | tail -n 1
git clean -fdX > /dev/null
dotnet restoreВ ошибках, если таковые будут, найти недостающие пакеты и выкачать их вручную, повторить до тех пор пока рестор не отработает с успехом
Условно в моем случае оно ругалось мол нет Microsoft.NETCore.App.Ref, ну ок, выполнил nucache Microsoft.NETCore.App.Ref, но затем оно начало ругаться мол нет Microsoft.NETCore.App.Ref версии Х, ну ок, выполнил nucache Microsoft.NETCore.App.Ref X, но и на этом не все, с какого то перепугу, какой то транзитивной зависимости или еще чему то, нужна версия Y, и только после ее установки, все отресторилось
worflow проверки
# run me once
dotnet nuget locals all --clear | tail -n 1
# switch offline
nucache offline
# navigate to project
cd ~/github.com/rabotaua/published-vacancies-api
# cleanup
git clean -fdX
# restore
dotnet restore
# for not found packages run:
nucache Foo 1.2.3
nucache Bar 4.5.6
# repeat
git clean -fdX
dotnet restore
# navigate to next project
cd ../apply-api
# repeat cleanup/restore/install stepsNuCache V1
Для полноценной разработки уже есть необходимые мини контейнеры и казалось бы все ок, НО, в случае переключения веток Rider может захотеть обновить пакеты NuGet после чего все превращается в тыкву 🤷♂️
Благо в NuGet есть встроенный механизм для подобного сценария, а именно, в качестве источника пакетов можно указать локальную папку и сложить туда все nupkg файлы которые могут понадобиться
На деле это выглядит как то вот так:
echo 'recreate offline directory for demo'
rm -rf offline || true
mkdir offline
echo 'create demo classlib'
rm -rf mylib || true
mkdir mylib
cd mylib
dotnet new console
dotnet pack
cd ..
echo 'pretend like we downloaded some nupkg for offline usage'
mv mylib/bin/Debug/mylib.1.0.0.nupkg ./offline/
echo 'list current nuget sources'
dotnet nuget list source
echo 'remove offline source if any'
dotnet nuget remove source offline
echo 'add offline source'
dotnet nuget add source "${PWD}/offline" --name=offline
echo 'remove original nuget.org source'
dotnet nuget remove source nuget.org
echo 'demo of offline usage'
rm -rf consoleapp || true
mkdir consoleapp
cd consoleapp
dotnet new console
dotnet add package mylib
dotnet restore
cd ..
echo 'add nuget.org back'
dotnet nuget add source https://api.nuget.org/v3/index.json --name=nuget.org
echo 'cleanup'
rm -rf consoleapp mylib offline || trueТо есть мы можем полноценно работать даже без интернета, но для этого нам понадобиться выкачать все зависимости которые у нас есть и\или могут понадобиться
Для этого делаем простой скрипт для обхода проектов по типу такого:
$directories = @(
'~/Downloads/gitsearch/github.com/rabotaua',
'~/github.com/rabotaua',
'~/Desktop',
'~/Downloads',
'~/github.com/mac2000/notes'
)
foreach($directory in $directories) {
$projects = Get-ChildItem -Path $directory -Recurse -Depth 4 -Include '\*.csproj'
foreach($project in $projects) {
$xml = \[xml\](Get-Content $project.FullName)
foreach($reference in $xml.Project.ItemGroup.PackageReference) {
if (-not $reference) { continue }
$packages += \[PSCustomObject\]@{
Package = $reference.Include
Version = $reference.Version
}
}
}
}
$packages = $packages | Select-Object Package, Version -UniqueНа выходе у нас будет массив пакетов которые пользуются в найденных проектах
Примечание: есть ряд пакетов которые нужны но не указываются в зависимостях, об этом чуть ниже, но на деле все равно есть смысл перепроверить как все работает пока есть сеть
Теперь нам нужно скачать каждый пакет, по ссылке:
Примечания:
- Сразу же следом окажется что у пакетов есть зависимости, и надо бы выкачать и их
- Префикс
v3-flatcontainerв теории может быть другим и документация рекомендуем не предполагать это дело, а забирать из мета данных пакета
NuGet API 101
Точка входа:
https://api.nuget.org/v3/index.json
От сюда нам понадобяться ссылки:
RegistrationsBaseUrl- префикс по которому мы будем забирать ссылки на скачивание пакета и его информациюPackageBaseAddress- префикс по которому мы будем забирать список доступных версий пакета
Примеры ссылок для Microsoft.Extensions.Caching.StackExchangeRedis версии 7.0.0:
- versions - список доступных версий, нам это нужно в случае если версия не задана явно или используется wildcard
- registration - тут нас интересуют
packageContentсодержащий ссылку для скачивания иcatalogEntryсо ссылкой на детали о пакете, откуда мы заберем его зависимости и хэш сумму скачиваемого файла - catalog - тут нас интересуют
dependencyGroupsсодержащие список зависимостей иpackageHash
How it works
Имея искомый пакет и опционально версию мы хотим:
- если версия не задана, вытянуть список доступных версий и взять последнюю стабильную
- если версия задана со звездочкой - вытянуть список версий и взять последнюю подходящую
- итерируясь по доступным префиксам, забрать registration и catalog для искомой версии пакета
- проверить наличие файла на локальной файловой системе и совпадения его хэша, в случае необходимости выкачать файл
- рекурсивно повторить для всех зависимостей пакета
Примечание: поскольку пакетов получается ну очень много и по концовке очень большое кол-во пересечений, дабы не повторять одно и тоже для условного System.Test.Whatever было бы не плохо кэшировать результаты запросов, иначе процесс занимает вечность
PowerShell implementation v0.1
У меня получилось вот такое (не претендую на качество кода, бо делалось на коленке еще и с переменно доступным интернетом что бы проверить работает ли, да и сделать то можно на C# или на чем угодно, тут уже каждый сам себе повар и готовит как захочет)
function NuCache($package, $version) {
$dir = '/Users/mac/nucache'
if (-not $global:NuCacheRegistrationsBaseUrls) {
$global:NuCacheRegistrationsBaseUrls = Invoke-RestMethod 'https://api.nuget.org/v3/index.json' | Select-Object -ExpandProperty resources | Where-Object { $\_.'@type'.StartsWith('RegistrationsBaseUrl') } | Select-Object -ExpandProperty '@id' -Unique
}
if (-not $global:NuCachePackageBaseAddress) {
$global:NuCachePackageBaseAddress = Invoke-RestMethod 'https://api.nuget.org/v3/index.json' | Select-Object -ExpandProperty resources | Where-Object '@type' -EQ 'PackageBaseAddress/3.0.0' | Select-Object -ExpandProperty '@id'
}
if (-not $global:NuCacheRegistrations) {
$global:NuCacheRegistrations = @{}
}
if (-not $global:NuCacheVersions) {
$global:NuCacheVersions = @{}
}
if (-not $global:NuCacheCatalogEntries) {
$global:NuCacheCatalogEntries = @{}
}
$package = $package.ToLower()
if ($version -and $version -match '^\\\[\\d+\\.\\d+\\.\\d+,\\d+\\.\\d+\\.\\d+\\\]$') {
$version = $version.Split(',')\[0\].Trim('\[')
}
if (-not $version -or $version.Contains('\*')) {
if (-not $global:NuCacheVersions\[$package\]) {
$global:NuCacheVersions\[$package\] = Invoke-RestMethod "$($global:NuCachePackageBaseAddress)$($package)/index.json" | Select-Object -ExpandProperty versions
Write-Host "$package - $($global:NuCacheVersions\[$package\].Count) versions retrieved"
}
$last = $global:NuCacheVersions\[$package\] | Where-Object { -not $\_.Contains('-') } | Select-Object -Last 1
if (-not $last) {
$last = $global:NuCacheVersions\[$package\] | Select-Object -Last 1
}
if ($version -and $version.Contains('\*')) {
$wildcard = $global:NuCacheVersions\[$package\] | Where-Object { $\_ -match $version } | Select-Object -Last 1
if ($wildcard) {
$version = $wildcard
} else {
$version = $last
}
} else {
$version = $last
}
}
$file = "$($package).$($version).nupkg"
$target = Join-Path -Path $dir -ChildPath $file
if (-not $global:NuCacheRegistrations\[$file\]) {
$registration = $null
foreach($registrationsBaseUrl in $global:NuCacheRegistrationsBaseUrls) {
try {
$registration = Invoke-RestMethod "$($registrationsBaseUrl)$($package)/$($version).json"
Write-Host "$package $version - registration retrieved"
break
} catch {}
}
$global:NuCacheRegistrations\[$file\] = $registration
}
if (-not $global:NuCacheRegistrations\[$file\]) {
if (-not $global:NuCacheVersions\[$package\]) {
$global:NuCacheVersions\[$package\] = Invoke-RestMethod "$($global:NuCachePackageBaseAddress)$($package)/index.json" | Select-Object -ExpandProperty versions
Write-Host "$package - $($global:NuCacheVersions\[$package\].Count) versions retrieved"
}
$v = $global:NuCacheVersions\[$package\] | Where-Object { $\_ -match \[regex\]::Replace($version, "\\.\\d+$", "") } | Select-Object -Last 1
if (-not $v) {
$v = $global:NuCacheVersions\[$package\] | Where-Object { $\_ -match \[regex\]::Replace($version, "\\.\\d+\\.\\d+$", "") } | Select-Object -Last 1
}
if (-not $v) {
$v = $global:NuCacheVersions\[$package\] | Where-Object { $\_ -match \[regex\]::Replace($version, "\\d+$", "") } | Select-Object -Last 1
}
if ($v) {
NuCache $package $v
} else {
Write-Host "$package $version - not found"
}
return
}
if (-not $global:NuCacheCatalogEntries.ContainsKey($file)) {
$global:NuCacheCatalogEntries\[$file\] = Invoke-RestMethod $global:NuCacheRegistrations\[$file\].catalogEntry
}
$exists = Test-Path $target
if ($exists) {
$alg = $global:NuCacheCatalogEntries\[$file\].packageHashAlgorithm
$wantedHash = $global:NuCacheCatalogEntries\[$file\].packageHash
if ($alg -eq 'SHA512') {
$hasher = \[System.Security.Cryptography.SHA512\]::Create()
} elseif ($alg -eq 'SHA256') {
$hasher = \[System.Security.Cryptography.SHA256\]::Create()
} elseif ($alg -eq 'SHA1') {
$hasher = \[System.Security.Cryptography.SHA1\]::Create()
} elseif ($alg -eq 'MD5') {
$hasher = \[System.Security.Cryptography.MD5\]::Create()
} else {
Write-Host "Unknown $alg for $package $version"
}
if ($hasher) {
$stream = \[System.IO.File\]::OpenRead($target)
$bytes = $hasher.ComputeHash($stream)
$actualHash = \[System.Convert\]::ToBase64String($bytes)
$stream.Dispose()
$hasher.Dispose()
if ($actualHash -ne $wantedHash) {
$exists = $false
}
}
}
if (-not $exists) {
$ProgressPreference = 'SilentlyContinue' # Subsequent calls do not display UI.
Invoke-WebRequest -Uri ($global:NuCacheRegistrations\[$file\].packageContent) -OutFile $target
$ProgressPreference = 'Continue' # Subsequent calls do display UI.
Write-Host "$package $version - downloaded"
} else {
# Write-Host "$package $version - exists"
}
$deps = $global:NuCacheCatalogEntries\[$file\] | Select-Object -ExpandProperty dependencyGroups -ErrorAction SilentlyContinue | Where-Object dependencies -NE $null | Select-Object -ExpandProperty dependencies | Select-Object id, range -Unique | Select-Object @{n='Package';e={ $\_.id.ToLower() }}, @{n='Version';e={ $\_.range.Split(',')\[0\].TrimStart('\[').TrimStart('(') }}
foreach($d in $deps) {
$df = "$($d.Package).$($d.Version).nupkg"
$dt = Join-Path -Path $dir -ChildPath $df
if (Test-Path $dt) {
# Write-Host "$($d.Package) $($d.Version) - exists"
continue
}
NuCache $d.Package $d.Version
}
}Соотв с этой ф-ией можно:
# download concrete package version and all its dependencies
NuCache 'Microsoft.Extensions.Caching.StackExchangeRedis' '7.0.0'
# download latest version and all its dependencies
NuCache 'Newtonsoft.Json'
Ну и остается собрать все в кучу по типу такого:
$packages = @(
# list of wanted low level packages that are somehow were not listed/downloaded
\[PSCustomObject\]@{ Package = 'Microsoft.AspNetCore.App.Ref'; Version = $null },
\[PSCustomObject\]@{ Package = 'Microsoft.NETCore.App.Ref'; Version = $null },
\[PSCustomObject\]@{ Package = 'Microsoft.AspNetCore.App.Ref'; Version = '6.0.11' },
\[PSCustomObject\]@{ Package = 'Microsoft.NETCore.App.Ref'; Version = '6.0.11' },
\[PSCustomObject\]@{ Package = 'Microsoft.AspNetCore.App.Ref'; Version = '5.0.0' },
\[PSCustomObject\]@{ Package = 'Microsoft.NETCore.App.Ref'; Version = '5.0.0' },
\[PSCustomObject\]@{ Package = 'Microsoft.AspNetCore.App.Ref'; Version = '3.1.0' },
\[PSCustomObject\]@{ Package = 'Microsoft.NETCore.App.Ref'; Version = '3.1.0' },
\[PSCustomObject\]@{ Package = 'Microsoft.AspNetCore.App.Ref'; Version = '3.1.10' },
\[PSCustomObject\]@{ Package = 'Microsoft.NETCore.App.Ref'; Version = '3.1.10' },
\[PSCustomObject\]@{ Package = 'System.Reflection.Extensions'; Version = '4.3.0' },
\[PSCustomObject\]@{ Package = 'System.Reflection.Emit'; Version = '4.6.0' },
\[PSCustomObject\]@{ Package = 'System.Reflection.Emit.Lightweight'; Version = '4.6.0' },
\[PSCustomObject\]@{ Package = 'System.Reflection.Metadata'; Version = '1.4.1' },
\[PSCustomObject\]@{ Package = 'System.Reflection.Metadata'; Version = '1.3.0' },
\[PSCustomObject\]@{ Package = 'Namotion.Reflection'; Version = $null },
\[PSCustomObject\]@{ Package = 'Namotion.Reflection'; Version = '1.0.8' },
\[PSCustomObject\]@{ Package = 'Namotion.Reflection'; Version = '1.0.11' },
\[PSCustomObject\]@{ Package = 'Namotion.Reflection'; Version = '1.0.15' },
\[PSCustomObject\]@{ Package = 'Namotion.Reflection'; Version = '2.0.9' },
\[PSCustomObject\]@{ Package = 'Namotion.Reflection'; Version = '2.1.0' },
\[PSCustomObject\]@{ Package = 'Namotion.Reflection'; Version = '2.1.1' },
\[PSCustomObject\]@{ Package = 'Microsoft.NETCore.App.Host.osx-x64'; Version = '5.0.17' },
\[PSCustomObject\]@{ Package = 'Microsoft.NETCore.App.Host.osx-x64'; Version = '3.1.31' },
\[PSCustomObject\]@{ Package = 'Microsoft.Extensions.Hosting.Abstractions'; Version = '7.0.0' },
\[PSCustomObject\]@{ Package = 'Microsoft.Extensions.Hosting.Abstractions'; Version = '6.0.0' }
)
$directories = @(
'~/Downloads/gitsearch/github.com/rabotaua',
'~/github.com/rabotaua',
'~/Desktop',
'~/Downloads',
'~/github.com/mac2000/notes'
)
foreach($directory in $directories) {
$projects = Get-ChildItem -Path $directory -Recurse -Depth 4 -Include '\*.csproj'
foreach($project in $projects) {
$xml = \[xml\](Get-Content $project.FullName)
foreach($reference in $xml.Project.ItemGroup.PackageReference) {
if (-not $reference) { continue }
$packages += \[PSCustomObject\]@{
Package = $reference.Include
Version = $reference.Version
}
}
}
}
$packages = $packages | Select-Object Package, Version -Unique
foreach($item in $packages) {
NuCache $item.Package $item.Version
}Примечания:
- в зависимости от кол-ва проектов процесс может занять час другой, в моем случае выкачалось около 3.7K общим весом в 4.2Gb
- по каким то причинам пакеты по типу
Microsoft.NETCore.App.Refне числяться в списке зависимостей и вот уже думаешь мол все ок, выключают свет\интернет, а оно не работает потому лучше перепроверять по месту на конретных проектах
How to use
Команды которыми можно туда сюда переключаться
# list sources
dotnet nuget list source
# offline
dotnet nuget remove source nuget.org
dotnet nuget add source "/Users/mac/nucache" --name=nucache
# online
dotnet nuget remove source nucache
dotnet nuget add source https://api.nuget.org/v3/index.json --name=nuget.orgHow to check
У себя я делал следующее, в папке произвольного проекта:
# clean nuget cache
dotnet nuget locals all --clear | tail -n 1
# clear all bin, obj, packages, ... in repo
git clean -fdX > /dev/null
dotnet restoreИ в случае наличия ошибок в ресторе, просто добавлял “проблемные” пакеты в список выкачивалки и затягивал их скриптом
Alternative approaches
Альтернативно можно попробовать поднять кэширующую проксю, тот же nginx, но там есть сложности с редиректами, плюс все равно нужно будет как то скриптовать выкачку всех зависимостей и прочего.
Можно накалякать свою проксю на C#, Golang но нет смысла т.к. будет та же история что и с nginx
Если переписывать на C# можно заюзать клиента там куча плюшек из коробки.
Side effect observations
Пару любопытных фактов, вытянул все репозитории, из них все пакеты
Всего в выборке 667 проектов и 1302 пакета
Top 10 packages
- Newtonsoft.Json - 123
- Dapper - 84
- Rabota.Data.SqlClient - 84
- AutoMapper - 72
- Rabota.Kafka.Events - 66
- Microsoft.NET.Test.Sdk - 64
- Microsoft.ApplicationInsights - 63
- xunit - 62
- xunit.runner.visualstudio - 62
- coverlet.collector - 61
Примечания:
- любопытно что Newtonsoft.Json на первом месте, хотя казалось бы давно уже все на System.Test.Json
- в топ 10 попали библиотеки для тестирования, хотя тестов то особо как бы не шибко много
Top 10 versions
- Newtonsoft.Json.13.0.1 - 55
- xunit.2.4.1 - 43
- Rabota.Data.SqlClient.3.0.10 - 40
- AutoMapper.10.1.1 - 39
- Polly.Extensions.Http.3.0.0 - 38
- xunit.runner.visualstudio.2.4.3 - 38
- Dapper.2.0.123 - 37
- Newtonsoft.Json.12.0.3 - 31
- Rabota.Cors.1.* - 28
- AutoMapper.Extensions.Microsoft.DependencyInjection.8.1.1 - 25
Примечание: понятное дело что этот списко кажеться сомнительным в качестве пользы, но самое интересное идет прямо следом
Top 10 spread/outdated
- Rabota.Kafka.Events - 20
- Microsoft.AspNetCore.Authentication.JwtBearer - 18
- NEST - 18
- AWSSDK.S3 - 18
- Microsoft.AspNetCore.Mvc.Testing - 16
- NSwag.CodeGeneration.CSharp - 15
- NSwag.MSBuild - 15
- Microsoft.ApplicationInsights.AspNetCore - 15
- StackExchange.Redis - 15
- Microsoft.Extensions.Http.Polly - 14
Sample of spreaded/outdated package
- Rabota.Kafka.Events 1.* - 24
- Rabota.Kafka.Events 1.4.56 - 2
- Rabota.Kafka.Events 1.4.* - 14
- Rabota.Kafka.Events 1.4.10 - 2
- Rabota.Kafka.Events 1.4.4 - 1
- Rabota.Kafka.Events 1.2.4 - 4
- Rabota.Kafka.Events 1.2.59 - 2
- Rabota.Kafka.Events 1.4.1 - 2
- Rabota.Kafka.Events 1.4.43 - 1
- Rabota.Kafka.Events 1.4.55 - 2
- Rabota.Kafka.Events 1.2.* - 1
- Rabota.Kafka.Events 1.4.51 - 1
- Rabota.Kafka.Events 1.2.38 - 2
- Rabota.Kafka.Events 1.4.54 - 2
- Rabota.Kafka.Events 1.4.18 - 1
- Rabota.Kafka.Events 1.3.67 - 1
- Rabota.Kafka.Events 1.2.57 - 1
- Rabota.Kafka.Events 1.2.37 - 1
- Rabota.Kafka.Events 1.4.50 - 1
- Rabota.Kafka.Events 1.4.25 - 1
Вот такие любопытные пироги