From c8b6d34c15f5bf99c386051da3edc61155c266b6 Mon Sep 17 00:00:00 2001 From: Joel Winarske Date: Fri, 19 Jun 2026 17:16:58 -0700 Subject: [PATCH] fix(cross): read deb822 apt sources (trixie/raspios) for -dev resolution MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The arm-gnu provider resolved the -dev closure only from one-line `deb …` sources (sources.list + sources.list.d/*.list). Trixie (Debian 13) and current raspios ship their sources in deb822 format (sources.list.d/*.sources with Types/URIs/Suites/Components), so resolution failed with "no apt sources … (cannot resolve -dev)". - aptIndexUrls now parses both formats: one-line entries and deb822 stanzas (multi-value URIs/Suites/Components, trailing-slash URIs normalized, deb-src and Enabled:no skipped). - _readAptSources also reads sources.list.d/*.sources and separates files with a blank line so one-line and deb822 stanzas never merge. Fixes the publish-trixie job; publish-bookworm (one-line sources) already worked. Tests cover deb822 and mixed one-line+deb822 inputs. Signed-off-by: Joel Winarske --- lib/src/cross/apt_resolver.dart | 85 ++++++++++++++++++----- lib/src/cross/arm_gnu_cross_provider.dart | 23 ++++-- test/src/cross/apt_resolver_test.dart | 46 ++++++++++++ 3 files changed, 131 insertions(+), 23 deletions(-) diff --git a/lib/src/cross/apt_resolver.dart b/lib/src/cross/apt_resolver.dart index 217f82b..a80115d 100644 --- a/lib/src/cross/apt_resolver.dart +++ b/lib/src/cross/apt_resolver.dart @@ -114,31 +114,82 @@ AptIndex parsePackagesIndex(String text, {required String repoBase}) { } /// Compressed `Packages`-index URLs for [arch], parsed from a sysroot's apt -/// sources (the one-line `deb [opts] ...` format). One URL -/// per (source, component). `deb-src`, comments, and blanks are skipped. +/// sources. Handles both the classic one-line `deb` format and the deb822 +/// `Types:/URIs:/Suites:/Components:` stanzas used by trixie/raspios. One URL +/// per (source, component); `deb-src`, disabled entries, comments, and blanks +/// are skipped. List aptIndexUrls(String sourcesText, String arch) { final urls = []; - for (final line in sourcesText.split('\n')) { - final t = line.trim(); - if (!t.startsWith('deb ')) continue; - var toks = t.substring(4).trim().split(RegExp(r'\s+')); - if (toks.isNotEmpty && toks.first.startsWith('[')) { - var i = 0; - while (i < toks.length && !toks[i].endsWith(']')) { - i++; + // Process blank-line-separated stanzas so deb822 entries (multi-line) don't + // bleed into one-line parsing. _readAptSources separates files with a blank + // line, so a one-line sources.list and a deb822 *.sources never mix. + for (final stanza in sourcesText.split(RegExp(r'\n[ \t]*\n'))) { + if (RegExp(r'^[ \t]*Types[ \t]*:', multiLine: true).hasMatch(stanza)) { + urls.addAll(_deb822IndexUrls(stanza, arch)); + } else { + for (final line in stanza.split('\n')) { + urls.addAll(_oneLineIndexUrls(line, arch)); } - toks = i + 1 < toks.length ? toks.sublist(i + 1) : const []; - } - if (toks.length < 3) continue; - final uri = toks[0]; - final suite = toks[1]; - for (final comp in toks.sublist(2)) { - urls.add('$uri/dists/$suite/$comp/binary-$arch/Packages.xz'); } } return urls; } +/// One classic `deb [opts] ` line -> binary index URLs. +List _oneLineIndexUrls(String line, String arch) { + final t = line.trim(); + if (!t.startsWith('deb ')) return const []; + var toks = t.substring(4).trim().split(RegExp(r'\s+')); + if (toks.isNotEmpty && toks.first.startsWith('[')) { + var i = 0; + while (i < toks.length && !toks[i].endsWith(']')) { + i++; + } + toks = i + 1 < toks.length ? toks.sublist(i + 1) : const []; + } + if (toks.length < 3) return const []; + final uri = toks[0]; + final suite = toks[1]; + return [ + for (final comp in toks.sublist(2)) + '$uri/dists/$suite/$comp/binary-$arch/Packages.xz', + ]; +} + +/// A deb822 stanza (`Types:`/`URIs:`/`Suites:`/`Components:`, as used by +/// trixie/raspios) -> binary index URLs for each URI x Suite x Component. +List _deb822IndexUrls(String stanza, String arch) { + final fields = {}; + String? key; + for (final line in stanza.split('\n')) { + final m = RegExp( + r'^([A-Za-z][A-Za-z-]*)[ \t]*:[ \t]*(.*)$', + ).firstMatch(line); + if (m != null) { + key = m.group(1); + fields[key!] = (m.group(2) ?? '').trim(); + } else if (key != null && (line.startsWith(' ') || line.startsWith('\t'))) { + fields[key] = '${fields[key]} ${line.trim()}'.trim(); + } + } + List values(String k) => (fields[k] ?? '') + .split(RegExp(r'\s+')) + .where((s) => s.isNotEmpty) + .toList(); + if (!values('Types').contains('deb')) return const []; + if ((fields['Enabled'] ?? 'yes').toLowerCase() == 'no') return const []; + final out = []; + for (final uri in values('URIs')) { + final base = uri.replaceFirst(RegExp(r'/+$'), ''); + for (final suite in values('Suites')) { + for (final comp in values('Components')) { + out.add('$base/dists/$suite/$comp/binary-$arch/Packages.xz'); + } + } + } + return out; +} + /// Names already installed in a sysroot's `/var/lib/dpkg/status` (plus what /// they Provide) — treated as already satisfied so they aren't re-downloaded. Set parseInstalled(String statusText) { diff --git a/lib/src/cross/arm_gnu_cross_provider.dart b/lib/src/cross/arm_gnu_cross_provider.dart index 9b1a32e..7a0759b 100644 --- a/lib/src/cross/arm_gnu_cross_provider.dart +++ b/lib/src/cross/arm_gnu_cross_provider.dart @@ -377,18 +377,29 @@ class ArmGnuCrossProvider implements CrossProvider { return null; } - /// Concatenate the sysroot's `/etc/apt/sources.list` and - /// `sources.list.d/*.list` for [aptIndexUrls]. + /// Concatenate the sysroot's `/etc/apt/sources.list`, the one-line + /// `sources.list.d/*.list`, and the deb822 `sources.list.d/*.sources` + /// (trixie/raspios) for [aptIndexUrls]. Files are separated by a blank line + /// so one-line and deb822 stanzas never merge. String _readAptSources(Directory sysrootDir) { final buf = StringBuffer(); - final main = File(p.join(sysrootDir.path, 'etc', 'apt', 'sources.list')); - if (main.existsSync()) buf.writeln(main.readAsStringSync()); + void add(File f) { + if (f.existsSync()) { + buf + ..writeln(f.readAsStringSync()) + ..writeln(); + } + } + + add(File(p.join(sysrootDir.path, 'etc', 'apt', 'sources.list'))); final dir = Directory( p.join(sysrootDir.path, 'etc', 'apt', 'sources.list.d'), ); if (dir.existsSync()) { - for (final f in dir.listSync().whereType()) { - if (f.path.endsWith('.list')) buf.writeln(f.readAsStringSync()); + final files = dir.listSync().whereType().toList() + ..sort((a, b) => a.path.compareTo(b.path)); + for (final f in files) { + if (f.path.endsWith('.list') || f.path.endsWith('.sources')) add(f); } } return buf.toString(); diff --git a/test/src/cross/apt_resolver_test.dart b/test/src/cross/apt_resolver_test.dart index a692cb0..0627ecc 100644 --- a/test/src/cross/apt_resolver_test.dart +++ b/test/src/cross/apt_resolver_test.dart @@ -114,5 +114,51 @@ deb-src http://deb.debian.org/debian bookworm main 'http://archive.raspberrypi.com/debian/dists/bookworm/main/binary-arm64/Packages.xz', ]); }); + + test('parses deb822 stanzas (trixie/raspios .sources)', () { + // URIs has a trailing slash (raspi.sources); Suites + Components are + // space-separated multi-value; deb-src and a disabled stanza are skipped. + const deb822 = ''' +Types: deb deb-src +URIs: http://deb.debian.org/debian +Suites: trixie trixie-updates +Components: main contrib + +Types: deb +URIs: http://archive.raspberrypi.com/debian/ +Suites: trixie +Components: main + +Types: deb +URIs: http://disabled.example/debian +Suites: trixie +Components: main +Enabled: no +'''; + expect(aptIndexUrls(deb822, 'arm64'), [ + 'http://deb.debian.org/debian/dists/trixie/main/binary-arm64/Packages.xz', + 'http://deb.debian.org/debian/dists/trixie/contrib/binary-arm64/Packages.xz', + 'http://deb.debian.org/debian/dists/trixie-updates/main/binary-arm64/Packages.xz', + 'http://deb.debian.org/debian/dists/trixie-updates/contrib/binary-arm64/Packages.xz', + // trailing slash on URIs is normalized (no //dists) + 'http://archive.raspberrypi.com/debian/dists/trixie/main/binary-arm64/Packages.xz', + ]); + }); + + test('one-line and deb822 files mix without bleeding', () { + // Mimics _readAptSources: files separated by a blank line. + const mixed = ''' +deb http://deb.debian.org/debian bookworm main + +Types: deb +URIs: http://archive.raspberrypi.com/debian +Suites: trixie +Components: main +'''; + expect(aptIndexUrls(mixed, 'arm64'), [ + 'http://deb.debian.org/debian/dists/bookworm/main/binary-arm64/Packages.xz', + 'http://archive.raspberrypi.com/debian/dists/trixie/main/binary-arm64/Packages.xz', + ]); + }); }); }