import 'dart:convert';
import 'dart:io';
class PackageEngineException implements Exception {
final String stderr;
PackageEngineException(this.stderr);
@override
String toString() => stderr.isEmpty ? 'dnf failed to run' : stderr;
}
class DeploymentInfo {
final String version;
final String checksum;
final String containerImageRef;
final String? baseChecksum;
final List<String> requestedPackages;
final bool booted;
final bool staged;
DeploymentInfo({
required this.version,
required this.checksum,
required this.containerImageRef,
this.baseChecksum,
this.requestedPackages = const [],
this.booted = false,
this.staged = false,
});
Map<String, dynamic> toJson() => {
'version': version,
'checksum': checksum,
'containerImageRef': containerImageRef,
'baseChecksum': baseChecksum,
'requestedPackages': requestedPackages,
'booted': booted,
'staged': staged,
};
factory DeploymentInfo.fromJson(Map<String, dynamic> json) => DeploymentInfo(
version: json['version'] as String,
checksum: json['checksum'] as String,
containerImageRef: json['containerImageRef'] as String,
baseChecksum: json['baseChecksum'] as String?,
requestedPackages:
(json['requestedPackages'] as List?)?.cast<String>() ?? const [],
booted: json['booted'] as bool? ?? false,
staged: json['staged'] as bool? ?? false,
);
static DeploymentInfo _fromStatusJson(Map<String, dynamic> d) => DeploymentInfo(
version: d['version'] as String? ?? 'unknown',
checksum: d['checksum'] as String? ?? 'unknown',
containerImageRef: d['container-image-reference'] as String? ?? 'unknown',
baseChecksum: d['base-checksum'] as String?,
requestedPackages: (d['requested-packages'] as List?)?.cast<String>() ?? const [],
booted: d['booted'] as bool? ?? false,
staged: d['staged'] as bool? ?? false,
);
}
class OstreeService {
static Future<DeploymentInfo> getStatus() async {
final deployments = await getAllDeployments();
return deployments.firstWhere(
(d) => d.booted,
orElse: () => deployments.first,
);
}
static Future<List<DeploymentInfo>> getAllDeployments() async {
final result = await Process.run('rpm-ostree', ['status', '--json']);
if (result.exitCode != 0) {
throw Exception('rpm-ostree status failed: ${result.stderr}');
}
final data = jsonDecode(result.stdout as String) as Map<String, dynamic>;
final deployments = data['deployments'] as List;
return deployments
.cast<Map<String, dynamic>>()
.map(DeploymentInfo._fromStatusJson)
.toList();
}
static Future<bool> checkPackageExists(String name) async {
final byName = await Process.run('dnf', ['repoquery', name]);
if (byName.exitCode != 0) {
throw PackageEngineException((byName.stderr as String).trim());
}
if ((byName.stdout as String).trim().isNotEmpty) return true;
final byProvides = await Process.run('dnf', ['repoquery', '--whatprovides', name]);
if (byProvides.exitCode != 0) {
throw PackageEngineException((byProvides.stderr as String).trim());
}
return (byProvides.stdout as String).trim().isNotEmpty;
}
static Future<String> inspectLocalRpm(String path) async {
if (!await File(path).exists()) {
throw Exception('No file found at "$path".');
}
final result = await Process.run(
'rpm',
['-qp', '--queryformat', '%{NAME}-%{VERSION}-%{RELEASE}.%{ARCH}', path],
);
if (result.exitCode != 0) {
final stderr = (result.stderr as String).trim();
throw Exception(stderr.isEmpty ? 'Not a valid RPM package.' : stderr);
}
return (result.stdout as String).trim();
}
static Future<int> installPackage(String target, void Function(String line) onLine) async {
final process = await Process.start('pkexec', ['rpm-ostree', 'install', '-y', target]);
process.stdout.transform(const SystemEncoding().decoder).listen((chunk) {
for (final line in chunk.split('\n')) {
if (line.isNotEmpty) onLine(line);
}
});
process.stderr.transform(const SystemEncoding().decoder).listen((chunk) {
for (final line in chunk.split('\n')) {
if (line.isNotEmpty) onLine(line);
}
});
return process.exitCode;
}
static Future<int> rollback(void Function(String line) onLine) async {
final process = await Process.start('pkexec', ['bootc', 'rollback', '--apply']);
process.stdout.transform(const SystemEncoding().decoder).listen((chunk) {
for (final line in chunk.split('\n')) {
if (line.isNotEmpty) onLine(line);
}
});
process.stderr.transform(const SystemEncoding().decoder).listen((chunk) {
for (final line in chunk.split('\n')) {
if (line.isNotEmpty) onLine(line);
}
});
return process.exitCode;
}
static Future<void> rebootNow() async {
await Process.start('pkexec', ['systemctl', 'reboot'], mode: ProcessStartMode.detached);
}
}