Compare commits
11 Commits
2e40732e69
...
b080dfe325
| Author | SHA1 | Date | |
|---|---|---|---|
| b080dfe325 | |||
| ce28a44e89 | |||
| 40dd06b8cf | |||
| 06d6a5ef45 | |||
| 4ea919432e | |||
| 6ccd83da5f | |||
| 89c41dfe4d | |||
| 8b599a7da7 | |||
| 6143ddc111 | |||
| a29c0e3cab | |||
| 3bcf8bc4f8 |
+89
-1
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "Brass-and-Sigil pack.lock.json - generated, do not edit by hand unless you know what you are doing",
|
||||
"name": "Brass and Sigil",
|
||||
"version": "0.12.0",
|
||||
"version": "0.16.0",
|
||||
"minecraft": "1.21.1",
|
||||
"loader": {
|
||||
"type": "neoforge",
|
||||
@@ -393,6 +393,94 @@
|
||||
"sha1": "f9a58fa95780f4b045d30559c1fdaedaa7f0fba3",
|
||||
"size": 295093,
|
||||
"side": "both"
|
||||
},
|
||||
{
|
||||
"source": "modrinth",
|
||||
"slug": "spark",
|
||||
"versionId": "v5qtqRQi",
|
||||
"version": "1.10.124-neoforge-1.21.1",
|
||||
"path": "mods/spark-1.10.124-neoforge.jar",
|
||||
"url": "https://cdn.modrinth.com/data/l6YH9Als/versions/v5qtqRQi/spark-1.10.124-neoforge.jar",
|
||||
"sha1": "9430cc2ab64ff89d698be593769fb9f9ee4efae6",
|
||||
"size": 3642581,
|
||||
"side": "both"
|
||||
},
|
||||
{
|
||||
"source": "modrinth",
|
||||
"slug": "appleskin",
|
||||
"versionId": "uAKA6Laj",
|
||||
"version": "3.0.9+mc1.21",
|
||||
"path": "mods/appleskin-neoforge-mc1.21-3.0.9.jar",
|
||||
"url": "https://cdn.modrinth.com/data/EsAfCjCV/versions/uAKA6Laj/appleskin-neoforge-mc1.21-3.0.9.jar",
|
||||
"sha1": "81cf0e668f991f83ac8820c386fbd6c9c3602246",
|
||||
"size": 75804,
|
||||
"side": "both"
|
||||
},
|
||||
{
|
||||
"source": "modrinth",
|
||||
"slug": "mouse-tweaks",
|
||||
"versionId": "9I21YYxf",
|
||||
"version": "1.21-2.26.1-neoforge",
|
||||
"path": "mods/MouseTweaks-neoforge-mc1.21-2.26.1.jar",
|
||||
"url": "https://cdn.modrinth.com/data/aC3cM3Vq/versions/9I21YYxf/MouseTweaks-neoforge-mc1.21-2.26.1.jar",
|
||||
"sha1": "6dae57f4f50f7808d2ed9a18f6cd1c0d4640c6bc",
|
||||
"size": 73938,
|
||||
"side": "client"
|
||||
},
|
||||
{
|
||||
"source": "modrinth",
|
||||
"slug": "farmers-delight",
|
||||
"versionId": "GbNuOZ4S",
|
||||
"version": "1.21.1-1.3.2",
|
||||
"path": "mods/FarmersDelight-1.21.1-1.3.2.jar",
|
||||
"url": "https://cdn.modrinth.com/data/R2OftAxM/versions/GbNuOZ4S/FarmersDelight-1.21.1-1.3.2.jar",
|
||||
"sha1": "bf7e7dede99e832de20e191b1f6c11f7a9b9a622",
|
||||
"size": 3163042,
|
||||
"side": "both"
|
||||
},
|
||||
{
|
||||
"source": "modrinth",
|
||||
"slug": "slice-and-dice",
|
||||
"versionId": "tyVnEa75",
|
||||
"version": "4.2.4",
|
||||
"path": "mods/sliceanddice-forge-4.2.4.jar",
|
||||
"url": "https://cdn.modrinth.com/data/GmjmRQ0A/versions/tyVnEa75/sliceanddice-forge-4.2.4.jar",
|
||||
"sha1": "13d8835813b3851d4818c59ec8f7124e4a826c14",
|
||||
"size": 406699,
|
||||
"side": "both"
|
||||
},
|
||||
{
|
||||
"source": "modrinth",
|
||||
"slug": "kotlin-for-forge",
|
||||
"versionId": "NrSebcsG",
|
||||
"version": "5.11.0",
|
||||
"path": "mods/kotlinforforge-5.11.0-all.jar",
|
||||
"url": "https://cdn.modrinth.com/data/ordsPcFz/versions/NrSebcsG/kotlinforforge-5.11.0-all.jar",
|
||||
"sha1": "4d83e8a8e8cb06b306d2cea1e903680fe513f1f3",
|
||||
"size": 6869790,
|
||||
"side": "both"
|
||||
},
|
||||
{
|
||||
"source": "modrinth",
|
||||
"slug": "balm",
|
||||
"versionId": "3XiinKbh",
|
||||
"version": "21.0.58+neoforge-1.21.1",
|
||||
"path": "mods/balm-neoforge-1.21.1-21.0.58.jar",
|
||||
"url": "https://cdn.modrinth.com/data/MBAkmtvl/versions/3XiinKbh/balm-neoforge-1.21.1-21.0.58.jar",
|
||||
"sha1": "47b6dbe51693d5a2b3eac15c6322f357de365f15",
|
||||
"size": 701868,
|
||||
"side": "both"
|
||||
},
|
||||
{
|
||||
"source": "modrinth",
|
||||
"slug": "waystones",
|
||||
"versionId": "PXntW965",
|
||||
"version": "21.1.33+neoforge-1.21.1",
|
||||
"path": "mods/waystones-neoforge-1.21.1-21.1.33.jar",
|
||||
"url": "https://cdn.modrinth.com/data/LOpKHB2A/versions/PXntW965/waystones-neoforge-1.21.1-21.1.33.jar",
|
||||
"sha1": "488bc70db3730f209e65cbda04c36dbc3918c448",
|
||||
"size": 878265,
|
||||
"side": "both"
|
||||
}
|
||||
],
|
||||
"defaultServer": {
|
||||
|
||||
@@ -0,0 +1,267 @@
|
||||
#requires -Version 7
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Audit pack.lock.json for missing required mod dependencies by reading
|
||||
each jar's mods.toml (and any jar-in-jar bundles) directly. Loader-truth,
|
||||
not Modrinth-truth.
|
||||
|
||||
.DESCRIPTION
|
||||
Each NeoForge/Forge mod declares its real dependencies in
|
||||
META-INF/neoforge.mods.toml (or META-INF/mods.toml) inside the jar.
|
||||
This is what the loader actually checks at startup; the Modrinth
|
||||
dependency field is metadata authors fill in (or don't).
|
||||
|
||||
The script also recurses into META-INF/jarjar/ bundles. Big mods like
|
||||
Create ship their deps (Registrate, Flywheel, Ponder) embedded as
|
||||
jar-in-jar -- the loader auto-loads them, so their modIds count as
|
||||
"present" without explicit lockfile entries.
|
||||
|
||||
Slug filter: only entries under "mods/" are treated as mod jars.
|
||||
Shaderpacks and other managed-but-non-mod files are skipped.
|
||||
|
||||
Language-provider jars (e.g. Kotlin for Forge) ship without a standard
|
||||
mods.toml; their modIds are added via a tiny override map.
|
||||
|
||||
Jars are cached in /tmp/bns-check-deps-cache/<sha1>.jar so subsequent
|
||||
runs are instant.
|
||||
|
||||
.PARAMETER LockPath
|
||||
.PARAMETER CacheDir
|
||||
.PARAMETER WarnOnly
|
||||
Print missing deps but exit 0. Deploy-Brass.ps1 calls without it.
|
||||
#>
|
||||
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[string]$LockPath = $(Join-Path (Split-Path -Parent $MyInvocation.MyCommand.Path) '..\pack\pack.lock.json'),
|
||||
[string]$CacheDir = '/tmp/bns-check-deps-cache',
|
||||
[switch]$WarnOnly
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$ProgressPreference = 'SilentlyContinue'
|
||||
|
||||
$LockPath = (Resolve-Path $LockPath).Path
|
||||
$lock = Get-Content $LockPath -Raw | ConvertFrom-Json -Depth 20
|
||||
if (-not $lock.mods) { throw "pack.lock.json has no 'mods' array." }
|
||||
if (-not (Test-Path $CacheDir)) { New-Item -ItemType Directory -Path $CacheDir | Out-Null }
|
||||
|
||||
# Vanilla / loader IDs are always satisfied -- they're never modpack mods.
|
||||
$vanillaIds = @('minecraft','neoforge','forge','fabric','fabricloader','quilt','java','javafml','lowcodefml')
|
||||
|
||||
# Language-provider / library jars that ship without a standard mods.toml.
|
||||
# Map their lockfile slug to the modId they register at runtime. Add entries
|
||||
# here as new ones are encountered.
|
||||
$slugToImplicitModId = @{
|
||||
'kotlin-for-forge' = 'kotlinforforge'
|
||||
}
|
||||
|
||||
Add-Type -AssemblyName System.IO.Compression
|
||||
Add-Type -AssemblyName System.IO.Compression.FileSystem
|
||||
|
||||
function Get-JarPath {
|
||||
param($mod)
|
||||
$cached = Join-Path $CacheDir ("$($mod.sha1).jar")
|
||||
if (Test-Path $cached) { return $cached }
|
||||
Write-Host " downloading $($mod.slug) ..." -ForegroundColor DarkGray
|
||||
try { Invoke-WebRequest -Uri $mod.url -OutFile $cached -TimeoutSec 60 }
|
||||
catch { throw "Failed to download $($mod.url): $($_.Exception.Message)" }
|
||||
return $cached
|
||||
}
|
||||
|
||||
# Read a specific entry's bytes from an already-open ZipArchive into memory.
|
||||
function Read-ZipEntry {
|
||||
param([System.IO.Compression.ZipArchive]$Zip, [string]$EntryName)
|
||||
$entry = $Zip.GetEntry($EntryName)
|
||||
if (-not $entry) { return $null }
|
||||
$ms = New-Object System.IO.MemoryStream
|
||||
$s = $entry.Open()
|
||||
try { $s.CopyTo($ms) } finally { $s.Dispose() }
|
||||
return $ms.ToArray()
|
||||
}
|
||||
|
||||
function Read-ZipEntryText {
|
||||
param([System.IO.Compression.ZipArchive]$Zip, [string]$EntryName)
|
||||
$entry = $Zip.GetEntry($EntryName)
|
||||
if (-not $entry) { return $null }
|
||||
$reader = New-Object System.IO.StreamReader($entry.Open())
|
||||
try { return $reader.ReadToEnd() }
|
||||
finally { $reader.Dispose() }
|
||||
}
|
||||
|
||||
# Tiny TOML reader -- only the bits we need (sections + key="value" lines).
|
||||
function Parse-ModsToml {
|
||||
param([string]$Text)
|
||||
$modIds = @()
|
||||
$deps = @()
|
||||
$section = $null
|
||||
$current = $null
|
||||
function Flush-Record {
|
||||
param($refModIds, $refDeps, $sec, $cur)
|
||||
if (-not $cur) { return }
|
||||
if ($sec -eq 'mods' -and $cur.modId) {
|
||||
$refModIds.Value += $cur.modId
|
||||
} elseif ($sec -like 'dep:*' -and $cur.modId) {
|
||||
$refDeps.Value += [pscustomobject]@{
|
||||
parent = $sec.Substring(4)
|
||||
modId = $cur.modId
|
||||
type = $cur.type
|
||||
mandatory = $cur.mandatory
|
||||
versionRange = $cur.versionRange
|
||||
}
|
||||
}
|
||||
}
|
||||
$modIdsRef = [ref]$modIds
|
||||
$depsRef = [ref]$deps
|
||||
foreach ($raw in ($Text -split "`n")) {
|
||||
$line = $raw.Trim()
|
||||
if (-not $line -or $line.StartsWith('#')) { continue }
|
||||
if ($line.StartsWith('[[')) {
|
||||
Flush-Record $modIdsRef $depsRef $section $current
|
||||
$current = $null
|
||||
if ($line -match '^\[\[mods\]\]') {
|
||||
$section = 'mods'; $current = @{}
|
||||
} elseif ($line -match '^\[\[dependencies\.([^\]]+)\]\]') {
|
||||
$section = "dep:$($Matches[1])"; $current = @{}
|
||||
} else {
|
||||
$section = $null
|
||||
}
|
||||
continue
|
||||
}
|
||||
if (-not $current) { continue }
|
||||
if ($line -match '^\s*([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.+?)\s*$') {
|
||||
$k = $Matches[1]; $v = $Matches[2].Trim()
|
||||
if ($v.StartsWith('"') -and $v.EndsWith('"')) { $v = $v.Substring(1, $v.Length - 2) }
|
||||
$current[$k] = $v
|
||||
}
|
||||
}
|
||||
Flush-Record $modIdsRef $depsRef $section $current
|
||||
return [pscustomobject]@{ ModIds = $modIdsRef.Value; Deps = $depsRef.Value }
|
||||
}
|
||||
|
||||
function Is-Required {
|
||||
param($dep)
|
||||
if ($dep.type) { return ($dep.type -eq 'required' -or $dep.type -eq 'mandatory') }
|
||||
if ($dep.mandatory) { return ($dep.mandatory -eq 'true') }
|
||||
return $true # NeoForge default
|
||||
}
|
||||
|
||||
# Recursively walk a jar (and its META-INF/jarjar/ embedded jars) collecting
|
||||
# every modId declared by mods.toml inside. Used to satisfy deps that ship
|
||||
# bundled (e.g. ponder/registrate/flywheel inside Create).
|
||||
function Collect-Jar-Identity {
|
||||
param([string]$JarPath)
|
||||
$modIds = @()
|
||||
$deps = @()
|
||||
$zip = [System.IO.Compression.ZipFile]::OpenRead($JarPath)
|
||||
try {
|
||||
# 1) This jar's own mods.toml (if any).
|
||||
$toml = Read-ZipEntryText -Zip $zip -EntryName 'META-INF/neoforge.mods.toml'
|
||||
if (-not $toml) { $toml = Read-ZipEntryText -Zip $zip -EntryName 'META-INF/mods.toml' }
|
||||
if ($toml) {
|
||||
$parsed = Parse-ModsToml -Text $toml
|
||||
$modIds += $parsed.ModIds
|
||||
$deps += $parsed.Deps
|
||||
}
|
||||
# 2) jar-in-jar bundles -- recursively extract + collect modIds (we only
|
||||
# need their modIds; their deps are the parent's problem to satisfy).
|
||||
$jarjarEntries = $zip.Entries | Where-Object {
|
||||
$_.FullName -like 'META-INF/jarjar/*.jar'
|
||||
}
|
||||
foreach ($e in $jarjarEntries) {
|
||||
$tmpInner = [System.IO.Path]::GetTempFileName() + '.jar'
|
||||
$s = $e.Open()
|
||||
try {
|
||||
$fs = [System.IO.File]::Open($tmpInner, [System.IO.FileMode]::Create)
|
||||
try { $s.CopyTo($fs) } finally { $fs.Dispose() }
|
||||
} finally { $s.Dispose() }
|
||||
$inner = Collect-Jar-Identity -JarPath $tmpInner
|
||||
$modIds += $inner.ModIds # bundled deps count as present
|
||||
# Don't propagate bundled deps -- they're satisfied within this jar
|
||||
Remove-Item $tmpInner -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
} finally {
|
||||
$zip.Dispose()
|
||||
}
|
||||
return [pscustomobject]@{ ModIds = ($modIds | Select-Object -Unique); Deps = $deps }
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "Auditing $($lock.mods.Count) lockfile entries (parsing each jar's mods.toml + jar-in-jar bundles)..."
|
||||
Write-Host ""
|
||||
|
||||
$presentModIds = New-Object 'System.Collections.Generic.HashSet[string]' ([System.StringComparer]::OrdinalIgnoreCase)
|
||||
foreach ($v in $vanillaIds) { [void]$presentModIds.Add($v) }
|
||||
|
||||
# parent modId -> list of dep records (so the missing report blames a modId, not a slug)
|
||||
$jarDepsByParentModId = @{}
|
||||
$skipped = @() # informational
|
||||
|
||||
$count = 0
|
||||
foreach ($m in $lock.mods) {
|
||||
$count++
|
||||
# Only consider entries that live under mods/ -- shaderpacks, configs etc.
|
||||
# are managed by the launcher but aren't mods.
|
||||
if (-not ($m.path -like 'mods/*')) {
|
||||
$skipped += "$($m.slug): not under mods/ ($($m.path))"
|
||||
continue
|
||||
}
|
||||
# Language-provider override (kotlin-for-forge etc.)
|
||||
if ($slugToImplicitModId.ContainsKey($m.slug)) {
|
||||
[void]$presentModIds.Add($slugToImplicitModId[$m.slug])
|
||||
}
|
||||
try {
|
||||
$jar = Get-JarPath -mod $m
|
||||
$ident = Collect-Jar-Identity -JarPath $jar
|
||||
foreach ($id in $ident.ModIds) {
|
||||
[void]$presentModIds.Add($id)
|
||||
}
|
||||
foreach ($id in $ident.ModIds) {
|
||||
$jarDepsByParentModId[$id] = $ident.Deps
|
||||
}
|
||||
if (-not $ident.ModIds -and -not $slugToImplicitModId.ContainsKey($m.slug)) {
|
||||
$skipped += "$($m.slug): no modId discovered (no mods.toml; consider adding to override map)"
|
||||
}
|
||||
} catch {
|
||||
$skipped += "$($m.slug): $($_.Exception.Message)"
|
||||
}
|
||||
if ($count % 10 -eq 0) { Write-Host " ...processed $count/$($lock.mods.Count)" -ForegroundColor DarkGray }
|
||||
}
|
||||
|
||||
# Pass 2: check each parent's required deps against the present set.
|
||||
$missing = @{}
|
||||
foreach ($parentId in $jarDepsByParentModId.Keys) {
|
||||
foreach ($dep in $jarDepsByParentModId[$parentId]) {
|
||||
if (-not (Is-Required $dep)) { continue }
|
||||
if (-not $presentModIds.Contains($dep.modId)) {
|
||||
if (-not $missing.ContainsKey($dep.modId)) {
|
||||
$missing[$dep.modId] = @{ parents = @() }
|
||||
}
|
||||
$missing[$dep.modId].parents += $parentId
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
Write-Host ("=" * 60)
|
||||
if ($missing.Count -eq 0) {
|
||||
Write-Host "All required deps satisfied. ($($presentModIds.Count) mod IDs in pack, incl. bundled jar-in-jar)" -ForegroundColor Green
|
||||
} else {
|
||||
Write-Host "MISSING REQUIRED DEPS: $($missing.Count) unique" -ForegroundColor Red
|
||||
Write-Host ""
|
||||
foreach ($depId in ($missing.Keys | Sort-Object)) {
|
||||
$parents = ($missing[$depId].parents | Sort-Object -Unique) -join ', '
|
||||
Write-Host " $depId" -ForegroundColor Red
|
||||
Write-Host " needed by: $parents" -ForegroundColor DarkGray
|
||||
}
|
||||
}
|
||||
|
||||
if ($skipped.Count -gt 0) {
|
||||
Write-Host ""
|
||||
Write-Host "Informational (skipped / unanalysable):" -ForegroundColor DarkGray
|
||||
foreach ($s in $skipped) { Write-Host " - $s" -ForegroundColor DarkGray }
|
||||
}
|
||||
|
||||
if ($missing.Count -gt 0 -and -not $WarnOnly) {
|
||||
throw "Missing required dependencies. Add them to pack/pack.lock.json (or pass -WarnOnly to bypass)."
|
||||
}
|
||||
@@ -118,6 +118,17 @@ if (($shouldRunPack -or $shouldRunLauncher) -and -not $DryRun -and -not $Force)
|
||||
}
|
||||
}
|
||||
|
||||
# ─── Pre-flight: required-dependency check ─────────────────────────────────
|
||||
# Walks each Modrinth mod in pack.lock.json, fetches its 'required'
|
||||
# dependencies, and refuses to deploy if any aren't already in the lockfile.
|
||||
# Catches the failure mode where adding a mod that hard-deps on (say)
|
||||
# kotlinforforge crash-loops the daemon at startup. Only runs when pack
|
||||
# content is changing; -Force skips it (rarely needed).
|
||||
if ($shouldRunPack -and -not $DryRun -and -not $Force) {
|
||||
Write-Host "Pre-flight: validating Modrinth dependency graph..." -ForegroundColor DarkGray
|
||||
& (Join-Path $here 'Check-Deps.ps1')
|
||||
}
|
||||
|
||||
# ─── Pre-flight: was the daemon already running? ───────────────────────────
|
||||
# We don't auto-stop it (kicks active players to fix a problem that isn't
|
||||
# actually broken -- the atomic swap is safe with the daemon running). But
|
||||
|
||||
Reference in New Issue
Block a user