Merge pull request #54 from toyota-connected/fix/apt-deb822-sources

fix(cross): read deb822 apt sources (trixie/raspios) for -dev resolution
This commit is contained in:
Joel Winarske
2026-06-19 17:19:08 -07:00
committed by GitHub
3 changed files with 131 additions and 23 deletions
+68 -17
View File
@@ -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] <uri> <suite> <comp>...` 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<String> aptIndexUrls(String sourcesText, String arch) {
final urls = <String>[];
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] <uri> <suite> <comp...>` line -> binary index URLs.
List<String> _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<String> _deb822IndexUrls(String stanza, String arch) {
final fields = <String, String>{};
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<String> 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 = <String>[];
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<String> parseInstalled(String statusText) {
+17 -6
View File
@@ -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<File>()) {
if (f.path.endsWith('.list')) buf.writeln(f.readAsStringSync());
final files = dir.listSync().whereType<File>().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();
+46
View File
@@ -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',
]);
});
});
}