Initial commit: Layer Wizard
A GUI installer wizard for permanent rpm-ostree package layers on bootc/image-based systems. Walks through: welcome/current-version check, package name entry with existence check, confirmation, then a streamed rpm-ostree install via pkexec. On success, writes a pending-confirmation marker and installs an autostart entry so the next login shows a "did this update work?" screen with a one-click rollback (bootc rollback --apply) if not. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
commit
e0cbf8ae20
22 changed files with 1599 additions and 0 deletions
48
.gitignore
vendored
Normal file
48
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
# Miscellaneous
|
||||
*.class
|
||||
*.log
|
||||
*.pyc
|
||||
*.swp
|
||||
.DS_Store
|
||||
.atom/
|
||||
.build/
|
||||
.buildlog/
|
||||
.history
|
||||
.svn/
|
||||
.swiftpm/
|
||||
migrate_working_dir/
|
||||
|
||||
# IntelliJ related
|
||||
*.iml
|
||||
*.ipr
|
||||
*.iws
|
||||
.idea/
|
||||
|
||||
# The .vscode folder contains launch configuration and tasks you configure in
|
||||
# VS Code which you may wish to be included in version control, so this line
|
||||
# is commented out by default.
|
||||
#.vscode/
|
||||
|
||||
# Flutter/Dart/Pub related
|
||||
**/doc/api/
|
||||
**/ios/Flutter/.last_build_id
|
||||
.dart_tool/
|
||||
.flutter-plugins-dependencies
|
||||
.pub-cache/
|
||||
.pub/
|
||||
/build/
|
||||
/coverage/
|
||||
|
||||
# Symbolication related
|
||||
app.*.symbols
|
||||
|
||||
# Obfuscation related
|
||||
app.*.map.json
|
||||
|
||||
# Android Studio will place build artifacts here
|
||||
/android/app/debug
|
||||
/android/app/profile
|
||||
/android/app/release
|
||||
|
||||
# Widget Preview related
|
||||
.widget_preview/
|
||||
30
.metadata
Normal file
30
.metadata
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
# This file tracks properties of this Flutter project.
|
||||
# Used by Flutter tool to assess capabilities and perform upgrades etc.
|
||||
#
|
||||
# This file should be version controlled and should not be manually edited.
|
||||
|
||||
version:
|
||||
revision: "5fc346839b5d0eef006ed8404392afb4dfae428d"
|
||||
channel: "stable"
|
||||
|
||||
project_type: app
|
||||
|
||||
# Tracks metadata for the flutter migrate command
|
||||
migration:
|
||||
platforms:
|
||||
- platform: root
|
||||
create_revision: 5fc346839b5d0eef006ed8404392afb4dfae428d
|
||||
base_revision: 5fc346839b5d0eef006ed8404392afb4dfae428d
|
||||
- platform: linux
|
||||
create_revision: 5fc346839b5d0eef006ed8404392afb4dfae428d
|
||||
base_revision: 5fc346839b5d0eef006ed8404392afb4dfae428d
|
||||
|
||||
# User provided section
|
||||
|
||||
# List of Local paths (relative to this file) that should be
|
||||
# ignored by the migrate tool.
|
||||
#
|
||||
# Files that are not part of the templates will be ignored by default.
|
||||
unmanaged_files:
|
||||
- 'lib/main.dart'
|
||||
- 'ios/Runner.xcodeproj/project.pbxproj'
|
||||
17
README.md
Normal file
17
README.md
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
# layer_wizard
|
||||
|
||||
A new Flutter project.
|
||||
|
||||
## Getting Started
|
||||
|
||||
This project is a starting point for a Flutter application.
|
||||
|
||||
A few resources to get you started if this is your first Flutter project:
|
||||
|
||||
- [Learn Flutter](https://docs.flutter.dev/get-started/learn-flutter)
|
||||
- [Write your first Flutter app](https://docs.flutter.dev/get-started/codelab)
|
||||
- [Flutter learning resources](https://docs.flutter.dev/reference/learning-resources)
|
||||
|
||||
For help getting started with Flutter development, view the
|
||||
[online documentation](https://docs.flutter.dev/), which offers tutorials,
|
||||
samples, guidance on mobile development, and a full API reference.
|
||||
33
analysis_options.yaml
Normal file
33
analysis_options.yaml
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
# This file configures the analyzer, which statically analyzes Dart code to
|
||||
# check for errors, warnings, and lints.
|
||||
#
|
||||
# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
|
||||
# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
|
||||
# invoked from the command line by running `flutter analyze`.
|
||||
|
||||
# The following line activates a set of recommended lints for Flutter apps,
|
||||
# packages, and plugins designed to encourage good coding practices.
|
||||
include: package:flutter_lints/flutter.yaml
|
||||
|
||||
analyzer:
|
||||
exclude:
|
||||
- build/**
|
||||
- linux/**
|
||||
|
||||
linter:
|
||||
# The lint rules applied to this project can be customized in the
|
||||
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
|
||||
# included above or to enable additional rules. A list of all available lints
|
||||
# and their documentation is published at https://dart.dev/lints.
|
||||
#
|
||||
# Instead of disabling a lint rule for the entire project in the
|
||||
# section below, it can also be suppressed for a single line of code
|
||||
# or a specific dart file by using the `// ignore: name_of_lint` and
|
||||
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
|
||||
# producing the lint.
|
||||
rules:
|
||||
# avoid_print: false # Uncomment to disable the `avoid_print` rule
|
||||
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
|
||||
|
||||
# Additional information about this file can be found at
|
||||
# https://dart.dev/guides/language/analysis-options
|
||||
88
lib/confirmation_marker.dart
Normal file
88
lib/confirmation_marker.dart
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'ostree_service.dart';
|
||||
|
||||
class PendingConfirmation {
|
||||
final String packageName;
|
||||
final DeploymentInfo before;
|
||||
final DateTime installedAt;
|
||||
|
||||
PendingConfirmation({
|
||||
required this.packageName,
|
||||
required this.before,
|
||||
required this.installedAt,
|
||||
});
|
||||
}
|
||||
|
||||
class ConfirmationMarker {
|
||||
static String get _stateDir => '${Platform.environment['HOME']}/.local/state/layer_wizard';
|
||||
static String get _markerPath => '$_stateDir/pending_confirmation.json';
|
||||
static String get _autostartPath =>
|
||||
'${Platform.environment['HOME']}/.config/autostart/com.ironmagma.layer_wizard.desktop';
|
||||
|
||||
static Future<void> write(String packageName, DeploymentInfo before) async {
|
||||
final dir = Directory(_stateDir);
|
||||
if (!await dir.exists()) {
|
||||
await dir.create(recursive: true);
|
||||
}
|
||||
final json = jsonEncode({
|
||||
'packageName': packageName,
|
||||
'before': before.toJson(),
|
||||
'installedAt': DateTime.now().toIso8601String(),
|
||||
});
|
||||
await File(_markerPath).writeAsString(json);
|
||||
}
|
||||
|
||||
static Future<PendingConfirmation?> read() async {
|
||||
final file = File(_markerPath);
|
||||
if (!await file.exists()) return null;
|
||||
try {
|
||||
final data = jsonDecode(await file.readAsString()) as Map<String, dynamic>;
|
||||
return PendingConfirmation(
|
||||
packageName: data['packageName'] as String,
|
||||
before: DeploymentInfo.fromJson(data['before'] as Map<String, dynamic>),
|
||||
installedAt: DateTime.parse(data['installedAt'] as String),
|
||||
);
|
||||
} catch (_) {
|
||||
// Corrupt/unreadable marker — treat as if nothing is pending rather
|
||||
// than getting the user stuck on a confirmation screen we can't render.
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
static Future<void> clear() async {
|
||||
final file = File(_markerPath);
|
||||
if (await file.exists()) {
|
||||
await file.delete();
|
||||
}
|
||||
}
|
||||
|
||||
/// Installs an autostart entry pointing at the currently-running binary,
|
||||
/// so the confirmation screen is shown automatically right after the next
|
||||
/// login. Idempotent.
|
||||
static Future<void> installAutostart() async {
|
||||
final dir = Directory('${Platform.environment['HOME']}/.config/autostart');
|
||||
if (!await dir.exists()) {
|
||||
await dir.create(recursive: true);
|
||||
}
|
||||
final exe = Platform.resolvedExecutable;
|
||||
final contents = '''
|
||||
[Desktop Entry]
|
||||
Type=Application
|
||||
Name=Layer Wizard Confirmation
|
||||
Comment=Confirm a pending permanent package layer
|
||||
Exec=$exe
|
||||
X-GNOME-Autostart-enabled=true
|
||||
NoDisplay=true
|
||||
''';
|
||||
await File(_autostartPath).writeAsString(contents);
|
||||
}
|
||||
|
||||
static Future<void> removeAutostart() async {
|
||||
final file = File(_autostartPath);
|
||||
if (await file.exists()) {
|
||||
await file.delete();
|
||||
}
|
||||
}
|
||||
}
|
||||
130
lib/confirmation_page.dart
Normal file
130
lib/confirmation_page.dart
Normal file
|
|
@ -0,0 +1,130 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'confirmation_marker.dart';
|
||||
import 'ostree_service.dart';
|
||||
import 'wizard_page.dart';
|
||||
|
||||
class ConfirmationPage extends StatefulWidget {
|
||||
final PendingConfirmation pending;
|
||||
|
||||
const ConfirmationPage({super.key, required this.pending});
|
||||
|
||||
@override
|
||||
State<ConfirmationPage> createState() => _ConfirmationPageState();
|
||||
}
|
||||
|
||||
class _ConfirmationPageState extends State<ConfirmationPage> {
|
||||
bool _rollingBack = false;
|
||||
final List<String> _log = [];
|
||||
final ScrollController _logScroll = ScrollController();
|
||||
|
||||
Future<void> _keep() async {
|
||||
await ConfirmationMarker.clear();
|
||||
await ConfirmationMarker.removeAutostart();
|
||||
if (!mounted) return;
|
||||
Navigator.of(context).pushReplacement(
|
||||
MaterialPageRoute(builder: (_) => const WizardHomePage()),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _confirmRollback() async {
|
||||
final confirmed = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('Roll back now?'),
|
||||
content: const Text(
|
||||
'This will immediately reboot the machine into the previous system '
|
||||
'version. Save any open work first.',
|
||||
),
|
||||
actions: [
|
||||
TextButton(onPressed: () => Navigator.pop(context, false), child: const Text('Cancel')),
|
||||
FilledButton(
|
||||
onPressed: () => Navigator.pop(context, true),
|
||||
style: FilledButton.styleFrom(backgroundColor: Theme.of(context).colorScheme.error),
|
||||
child: const Text('Roll back and reboot'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
if (confirmed != true) return;
|
||||
|
||||
setState(() => _rollingBack = true);
|
||||
// The machine reboots as part of this call returning — clear bookkeeping
|
||||
// first so there's nothing stale to confirm once we're back on the
|
||||
// known-good deployment.
|
||||
await ConfirmationMarker.clear();
|
||||
await ConfirmationMarker.removeAutostart();
|
||||
await OstreeService.rollback((line) {
|
||||
setState(() => _log.add(line));
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (_logScroll.hasClients) {
|
||||
_logScroll.jumpTo(_logScroll.position.maxScrollExtent);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final p = widget.pending;
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('Confirm Update')),
|
||||
body: Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: _rollingBack
|
||||
? [
|
||||
const Text('Rolling back and rebooting…', style: TextStyle(fontSize: 18)),
|
||||
const SizedBox(height: 16),
|
||||
Expanded(
|
||||
child: Container(
|
||||
color: Colors.black87,
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: ListView.builder(
|
||||
controller: _logScroll,
|
||||
itemCount: _log.length,
|
||||
itemBuilder: (context, i) => Text(
|
||||
_log[i],
|
||||
style: const TextStyle(color: Colors.white, fontFamily: 'monospace', fontSize: 12),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
]
|
||||
: [
|
||||
Icon(Icons.help_outline, size: 48, color: Theme.of(context).colorScheme.primary),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'You installed "${p.packageName}" on '
|
||||
'${p.installedAt.toLocal().toString().split('.').first}.',
|
||||
style: const TextStyle(fontSize: 18),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
const Text('The system rebooted into the update. Is everything working correctly?'),
|
||||
const SizedBox(height: 16),
|
||||
Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Text('Previous version: ${p.before.version}\n'
|
||||
'Previous image: ${p.before.containerImageRef}'),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
FilledButton.icon(
|
||||
onPressed: _keep,
|
||||
icon: const Icon(Icons.check),
|
||||
label: const Text('Yes, keep this update'),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
OutlinedButton.icon(
|
||||
onPressed: _confirmRollback,
|
||||
icon: const Icon(Icons.undo),
|
||||
label: const Text('No, something\'s wrong — roll back'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
45
lib/main.dart
Normal file
45
lib/main.dart
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'confirmation_marker.dart';
|
||||
import 'confirmation_page.dart';
|
||||
import 'wizard_page.dart';
|
||||
|
||||
void main() {
|
||||
runApp(const LayerWizardApp());
|
||||
}
|
||||
|
||||
class LayerWizardApp extends StatelessWidget {
|
||||
const LayerWizardApp({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MaterialApp(
|
||||
title: 'Layer Wizard',
|
||||
theme: ThemeData(colorSchemeSeed: Colors.teal, useMaterial3: true),
|
||||
home: const _StartupRouter(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Checks for a pending post-reboot confirmation before deciding which
|
||||
/// screen to show first.
|
||||
class _StartupRouter extends StatelessWidget {
|
||||
const _StartupRouter();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FutureBuilder<PendingConfirmation?>(
|
||||
future: ConfirmationMarker.read(),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState != ConnectionState.done) {
|
||||
return const Scaffold(body: Center(child: CircularProgressIndicator()));
|
||||
}
|
||||
final pending = snapshot.data;
|
||||
if (pending != null) {
|
||||
return ConfirmationPage(pending: pending);
|
||||
}
|
||||
return const WizardHomePage();
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
97
lib/ostree_service.dart
Normal file
97
lib/ostree_service.dart
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
class DeploymentInfo {
|
||||
final String version;
|
||||
final String checksum;
|
||||
final String containerImageRef;
|
||||
|
||||
DeploymentInfo({
|
||||
required this.version,
|
||||
required this.checksum,
|
||||
required this.containerImageRef,
|
||||
});
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
'version': version,
|
||||
'checksum': checksum,
|
||||
'containerImageRef': containerImageRef,
|
||||
};
|
||||
|
||||
factory DeploymentInfo.fromJson(Map<String, dynamic> json) => DeploymentInfo(
|
||||
version: json['version'] as String,
|
||||
checksum: json['checksum'] as String,
|
||||
containerImageRef: json['containerImageRef'] as String,
|
||||
);
|
||||
}
|
||||
|
||||
class OstreeService {
|
||||
/// Reads the currently booted deployment via `rpm-ostree status --json`
|
||||
/// (unprivileged, read-only).
|
||||
static Future<DeploymentInfo> getStatus() 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;
|
||||
final booted = deployments.cast<Map<String, dynamic>>().firstWhere(
|
||||
(d) => d['booted'] == true,
|
||||
orElse: () => deployments.first as Map<String, dynamic>,
|
||||
);
|
||||
return DeploymentInfo(
|
||||
version: booted['version'] as String? ?? 'unknown',
|
||||
checksum: booted['checksum'] as String? ?? 'unknown',
|
||||
containerImageRef: booted['container-image-reference'] as String? ?? 'unknown',
|
||||
);
|
||||
}
|
||||
|
||||
/// Unprivileged existence check so typos surface before escalating to
|
||||
/// pkexec. Matches on exact name or name.arch.
|
||||
static Future<bool> checkPackageExists(String name) async {
|
||||
final result = await Process.run('dnf', ['repoquery', name]);
|
||||
return result.exitCode == 0 && (result.stdout as String).trim().isNotEmpty;
|
||||
}
|
||||
|
||||
/// Permanently layers a package via `rpm-ostree install`. Streams combined
|
||||
/// stdout+stderr lines. Mirrors pkg_launcher's DnfService.installTransient
|
||||
/// pattern (pkexec through the desktop PolicyKit agent).
|
||||
static Future<int> installPackage(String name, void Function(String line) onLine) async {
|
||||
final process = await Process.start('pkexec', ['rpm-ostree', 'install', '-y', name]);
|
||||
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;
|
||||
}
|
||||
|
||||
/// Rolls back to the previously booted deployment and reboots into it
|
||||
/// immediately. Callers must warn the user before calling this: the
|
||||
/// screen will go black and the machine will restart as part of this
|
||||
/// call returning.
|
||||
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;
|
||||
}
|
||||
|
||||
/// Reboots immediately via the desktop PolicyKit agent.
|
||||
static Future<void> rebootNow() async {
|
||||
await Process.start('pkexec', ['systemctl', 'reboot'], mode: ProcessStartMode.detached);
|
||||
}
|
||||
}
|
||||
325
lib/wizard_page.dart
Normal file
325
lib/wizard_page.dart
Normal file
|
|
@ -0,0 +1,325 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'confirmation_marker.dart';
|
||||
import 'ostree_service.dart';
|
||||
|
||||
enum _ApplyState { idle, running, success, failed }
|
||||
|
||||
class WizardHomePage extends StatefulWidget {
|
||||
const WizardHomePage({super.key});
|
||||
|
||||
@override
|
||||
State<WizardHomePage> createState() => _WizardHomePageState();
|
||||
}
|
||||
|
||||
class _WizardHomePageState extends State<WizardHomePage> {
|
||||
int _currentStep = 0;
|
||||
final _packageController = TextEditingController();
|
||||
|
||||
Future<DeploymentInfo>? _statusFuture;
|
||||
bool _checking = false;
|
||||
String? _checkError;
|
||||
bool _packageVerified = false;
|
||||
|
||||
_ApplyState _applyState = _ApplyState.idle;
|
||||
final List<String> _log = [];
|
||||
final ScrollController _logScroll = ScrollController();
|
||||
DeploymentInfo? _beforeInfo;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_statusFuture = OstreeService.getStatus();
|
||||
}
|
||||
|
||||
Future<void> _checkPackage() async {
|
||||
final name = _packageController.text.trim();
|
||||
if (name.isEmpty) return;
|
||||
setState(() {
|
||||
_checking = true;
|
||||
_checkError = null;
|
||||
_packageVerified = false;
|
||||
});
|
||||
try {
|
||||
final exists = await OstreeService.checkPackageExists(name);
|
||||
setState(() {
|
||||
_packageVerified = exists;
|
||||
_checkError = exists ? null : 'No package named "$name" was found.';
|
||||
});
|
||||
} catch (e) {
|
||||
setState(() => _checkError = e.toString());
|
||||
} finally {
|
||||
setState(() => _checking = false);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _startInstall() async {
|
||||
final name = _packageController.text.trim();
|
||||
setState(() {
|
||||
_applyState = _ApplyState.running;
|
||||
_log.clear();
|
||||
});
|
||||
try {
|
||||
_beforeInfo = await OstreeService.getStatus();
|
||||
final exitCode = await OstreeService.installPackage(name, (line) {
|
||||
setState(() => _log.add(line));
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (_logScroll.hasClients) {
|
||||
_logScroll.jumpTo(_logScroll.position.maxScrollExtent);
|
||||
}
|
||||
});
|
||||
});
|
||||
if (exitCode == 0) {
|
||||
await ConfirmationMarker.write(name, _beforeInfo!);
|
||||
await ConfirmationMarker.installAutostart();
|
||||
setState(() => _applyState = _ApplyState.success);
|
||||
} else {
|
||||
setState(() => _applyState = _ApplyState.failed);
|
||||
}
|
||||
} catch (e) {
|
||||
setState(() {
|
||||
_log.add('Error: $e');
|
||||
_applyState = _ApplyState.failed;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void _onStepContinue() {
|
||||
if (_currentStep == 1) {
|
||||
if (!_packageVerified) return;
|
||||
}
|
||||
if (_currentStep == 2) {
|
||||
setState(() => _currentStep = 3);
|
||||
_startInstall();
|
||||
return;
|
||||
}
|
||||
if (_currentStep < 2) {
|
||||
setState(() => _currentStep += 1);
|
||||
}
|
||||
}
|
||||
|
||||
void _onStepCancel() {
|
||||
if (_currentStep > 0 && _applyState == _ApplyState.idle) {
|
||||
setState(() => _currentStep -= 1);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('Layer Wizard')),
|
||||
body: Stepper(
|
||||
type: StepperType.horizontal,
|
||||
currentStep: _currentStep,
|
||||
onStepContinue: _applyState == _ApplyState.idle ? _onStepContinue : null,
|
||||
onStepCancel: _applyState == _ApplyState.idle ? _onStepCancel : null,
|
||||
controlsBuilder: (context, details) {
|
||||
if (_currentStep == 3) return const SizedBox.shrink();
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(top: 16),
|
||||
child: Row(
|
||||
children: [
|
||||
FilledButton(
|
||||
onPressed: _currentStep == 1 && !_packageVerified ? null : details.onStepContinue,
|
||||
child: Text(_currentStep == 2 ? 'Install' : 'Continue'),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
if (_currentStep > 0)
|
||||
TextButton(onPressed: details.onStepCancel, child: const Text('Back')),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
steps: [
|
||||
Step(
|
||||
title: const Text('Welcome'),
|
||||
isActive: _currentStep >= 0,
|
||||
content: _buildWelcome(),
|
||||
),
|
||||
Step(
|
||||
title: const Text('Package'),
|
||||
isActive: _currentStep >= 1,
|
||||
content: _buildPackageStep(),
|
||||
),
|
||||
Step(
|
||||
title: const Text('Confirm'),
|
||||
isActive: _currentStep >= 2,
|
||||
content: _buildConfirmStep(),
|
||||
),
|
||||
Step(
|
||||
title: const Text('Apply'),
|
||||
isActive: _currentStep >= 3,
|
||||
content: _buildApplyStep(),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildWelcome() {
|
||||
return FutureBuilder<DeploymentInfo>(
|
||||
future: _statusFuture,
|
||||
builder: (context, snapshot) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'This permanently adds a package to your system image via '
|
||||
'"rpm-ostree install". Unlike a transient install, it survives '
|
||||
'a reboot — but it only takes effect after you restart.',
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
if (snapshot.hasData)
|
||||
Text('Currently running: ${snapshot.data!.version}\n'
|
||||
'${snapshot.data!.containerImageRef}')
|
||||
else if (snapshot.hasError)
|
||||
Text('Could not read system status: ${snapshot.error}')
|
||||
else
|
||||
const CircularProgressIndicator(),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPackageStep() {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
TextField(
|
||||
controller: _packageController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Package name',
|
||||
hintText: 'e.g. calligra-words',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
onChanged: (_) => setState(() => _packageVerified = false),
|
||||
onSubmitted: (_) => _checkPackage(),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
children: [
|
||||
OutlinedButton(
|
||||
onPressed: _checking ? null : _checkPackage,
|
||||
child: _checking
|
||||
? const SizedBox(width: 16, height: 16, child: CircularProgressIndicator(strokeWidth: 2))
|
||||
: const Text('Check'),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
if (_packageVerified)
|
||||
const Row(children: [Icon(Icons.check_circle, color: Colors.green), SizedBox(width: 4), Text('Found')]),
|
||||
],
|
||||
),
|
||||
if (_checkError != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 8),
|
||||
child: Text(_checkError!, style: TextStyle(color: Theme.of(context).colorScheme.error)),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildConfirmStep() {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('Package: ${_packageController.text.trim()}', style: const TextStyle(fontSize: 16)),
|
||||
const SizedBox(height: 12),
|
||||
const Text(
|
||||
'You will be prompted for your password via the system\'s '
|
||||
'PolicyKit dialog. Afterward, a restart will be required before '
|
||||
'the package becomes usable.',
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildApplyStep() {
|
||||
switch (_applyState) {
|
||||
case _ApplyState.idle:
|
||||
return const Text('Waiting to start…');
|
||||
case _ApplyState.running:
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
const LinearProgressIndicator(),
|
||||
const SizedBox(height: 12),
|
||||
SizedBox(height: 240, child: _buildLog()),
|
||||
],
|
||||
);
|
||||
case _ApplyState.success:
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(Icons.check_circle, color: Colors.green.shade600),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text('Restart required to use "${_packageController.text.trim()}".'),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
children: [
|
||||
FilledButton.icon(
|
||||
onPressed: () => OstreeService.rebootNow(),
|
||||
icon: const Icon(Icons.restart_alt),
|
||||
label: const Text('Reboot Now'),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
OutlinedButton(
|
||||
onPressed: () => setState(() {
|
||||
_currentStep = 0;
|
||||
_applyState = _ApplyState.idle;
|
||||
_packageController.clear();
|
||||
_packageVerified = false;
|
||||
_statusFuture = OstreeService.getStatus();
|
||||
}),
|
||||
child: const Text('Reboot Later'),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
SizedBox(height: 180, child: _buildLog()),
|
||||
],
|
||||
);
|
||||
case _ApplyState.failed:
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(Icons.error, color: Theme.of(context).colorScheme.error),
|
||||
const SizedBox(width: 8),
|
||||
const Text('Install failed. Nothing was changed — no restart needed.'),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
SizedBox(height: 180, child: _buildLog()),
|
||||
const SizedBox(height: 12),
|
||||
OutlinedButton(
|
||||
onPressed: () => setState(() => _applyState = _ApplyState.idle),
|
||||
child: const Text('Try Again'),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildLog() {
|
||||
return Container(
|
||||
color: Colors.black87,
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: ListView.builder(
|
||||
controller: _logScroll,
|
||||
itemCount: _log.length,
|
||||
itemBuilder: (context, i) => Text(
|
||||
_log[i],
|
||||
style: const TextStyle(color: Colors.white, fontFamily: 'monospace', fontSize: 12),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
1
linux/.gitignore
vendored
Normal file
1
linux/.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
flutter/ephemeral
|
||||
128
linux/CMakeLists.txt
Normal file
128
linux/CMakeLists.txt
Normal file
|
|
@ -0,0 +1,128 @@
|
|||
# Project-level configuration.
|
||||
cmake_minimum_required(VERSION 3.13)
|
||||
project(runner LANGUAGES CXX)
|
||||
|
||||
# The name of the executable created for the application. Change this to change
|
||||
# the on-disk name of your application.
|
||||
set(BINARY_NAME "layer_wizard")
|
||||
# The unique GTK application identifier for this application. See:
|
||||
# https://wiki.gnome.org/HowDoI/ChooseApplicationID
|
||||
set(APPLICATION_ID "com.ironmagma.layer_wizard")
|
||||
|
||||
# Explicitly opt in to modern CMake behaviors to avoid warnings with recent
|
||||
# versions of CMake.
|
||||
cmake_policy(SET CMP0063 NEW)
|
||||
|
||||
# Load bundled libraries from the lib/ directory relative to the binary.
|
||||
set(CMAKE_INSTALL_RPATH "$ORIGIN/lib")
|
||||
|
||||
# Root filesystem for cross-building.
|
||||
if(FLUTTER_TARGET_PLATFORM_SYSROOT)
|
||||
set(CMAKE_SYSROOT ${FLUTTER_TARGET_PLATFORM_SYSROOT})
|
||||
set(CMAKE_FIND_ROOT_PATH ${CMAKE_SYSROOT})
|
||||
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
|
||||
set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)
|
||||
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
|
||||
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
|
||||
endif()
|
||||
|
||||
# Define build configuration options.
|
||||
if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
|
||||
set(CMAKE_BUILD_TYPE "Debug" CACHE
|
||||
STRING "Flutter build mode" FORCE)
|
||||
set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS
|
||||
"Debug" "Profile" "Release")
|
||||
endif()
|
||||
|
||||
# Compilation settings that should be applied to most targets.
|
||||
#
|
||||
# Be cautious about adding new options here, as plugins use this function by
|
||||
# default. In most cases, you should add new options to specific targets instead
|
||||
# of modifying this function.
|
||||
function(APPLY_STANDARD_SETTINGS TARGET)
|
||||
target_compile_features(${TARGET} PUBLIC cxx_std_14)
|
||||
target_compile_options(${TARGET} PRIVATE -Wall -Werror)
|
||||
target_compile_options(${TARGET} PRIVATE "$<$<NOT:$<CONFIG:Debug>>:-O3>")
|
||||
target_compile_definitions(${TARGET} PRIVATE "$<$<NOT:$<CONFIG:Debug>>:NDEBUG>")
|
||||
endfunction()
|
||||
|
||||
# Flutter library and tool build rules.
|
||||
set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter")
|
||||
add_subdirectory(${FLUTTER_MANAGED_DIR})
|
||||
|
||||
# System-level dependencies.
|
||||
find_package(PkgConfig REQUIRED)
|
||||
pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0)
|
||||
|
||||
# Application build; see runner/CMakeLists.txt.
|
||||
add_subdirectory("runner")
|
||||
|
||||
# Run the Flutter tool portions of the build. This must not be removed.
|
||||
add_dependencies(${BINARY_NAME} flutter_assemble)
|
||||
|
||||
# Only the install-generated bundle's copy of the executable will launch
|
||||
# correctly, since the resources must in the right relative locations. To avoid
|
||||
# people trying to run the unbundled copy, put it in a subdirectory instead of
|
||||
# the default top-level location.
|
||||
set_target_properties(${BINARY_NAME}
|
||||
PROPERTIES
|
||||
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/intermediates_do_not_run"
|
||||
)
|
||||
|
||||
|
||||
# Generated plugin build rules, which manage building the plugins and adding
|
||||
# them to the application.
|
||||
include(flutter/generated_plugins.cmake)
|
||||
|
||||
|
||||
# === Installation ===
|
||||
# By default, "installing" just makes a relocatable bundle in the build
|
||||
# directory.
|
||||
set(BUILD_BUNDLE_DIR "${PROJECT_BINARY_DIR}/bundle")
|
||||
if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT)
|
||||
set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE)
|
||||
endif()
|
||||
|
||||
# Start with a clean build bundle directory every time.
|
||||
install(CODE "
|
||||
file(REMOVE_RECURSE \"${BUILD_BUNDLE_DIR}/\")
|
||||
" COMPONENT Runtime)
|
||||
|
||||
set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data")
|
||||
set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}/lib")
|
||||
|
||||
install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}"
|
||||
COMPONENT Runtime)
|
||||
|
||||
install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}"
|
||||
COMPONENT Runtime)
|
||||
|
||||
install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
|
||||
COMPONENT Runtime)
|
||||
|
||||
foreach(bundled_library ${PLUGIN_BUNDLED_LIBRARIES})
|
||||
install(FILES "${bundled_library}"
|
||||
DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
|
||||
COMPONENT Runtime)
|
||||
endforeach(bundled_library)
|
||||
|
||||
# Copy the native assets provided by the build.dart from all packages.
|
||||
set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/linux/")
|
||||
install(DIRECTORY "${NATIVE_ASSETS_DIR}"
|
||||
DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
|
||||
COMPONENT Runtime)
|
||||
|
||||
# Fully re-copy the assets directory on each build to avoid having stale files
|
||||
# from a previous install.
|
||||
set(FLUTTER_ASSET_DIR_NAME "flutter_assets")
|
||||
install(CODE "
|
||||
file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\")
|
||||
" COMPONENT Runtime)
|
||||
install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}"
|
||||
DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime)
|
||||
|
||||
# Install the AOT library on non-Debug builds only.
|
||||
if(NOT CMAKE_BUILD_TYPE MATCHES "Debug")
|
||||
install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
|
||||
COMPONENT Runtime)
|
||||
endif()
|
||||
88
linux/flutter/CMakeLists.txt
Normal file
88
linux/flutter/CMakeLists.txt
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
# This file controls Flutter-level build steps. It should not be edited.
|
||||
cmake_minimum_required(VERSION 3.10)
|
||||
|
||||
set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral")
|
||||
|
||||
# Configuration provided via flutter tool.
|
||||
include(${EPHEMERAL_DIR}/generated_config.cmake)
|
||||
|
||||
# TODO: Move the rest of this into files in ephemeral. See
|
||||
# https://github.com/flutter/flutter/issues/57146.
|
||||
|
||||
# Serves the same purpose as list(TRANSFORM ... PREPEND ...),
|
||||
# which isn't available in 3.10.
|
||||
function(list_prepend LIST_NAME PREFIX)
|
||||
set(NEW_LIST "")
|
||||
foreach(element ${${LIST_NAME}})
|
||||
list(APPEND NEW_LIST "${PREFIX}${element}")
|
||||
endforeach(element)
|
||||
set(${LIST_NAME} "${NEW_LIST}" PARENT_SCOPE)
|
||||
endfunction()
|
||||
|
||||
# === Flutter Library ===
|
||||
# System-level dependencies.
|
||||
find_package(PkgConfig REQUIRED)
|
||||
pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0)
|
||||
pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0)
|
||||
pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0)
|
||||
|
||||
set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/libflutter_linux_gtk.so")
|
||||
|
||||
# Published to parent scope for install step.
|
||||
set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE)
|
||||
set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE)
|
||||
set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE)
|
||||
set(AOT_LIBRARY "${PROJECT_DIR}/build/lib/libapp.so" PARENT_SCOPE)
|
||||
|
||||
list(APPEND FLUTTER_LIBRARY_HEADERS
|
||||
"fl_basic_message_channel.h"
|
||||
"fl_binary_codec.h"
|
||||
"fl_binary_messenger.h"
|
||||
"fl_dart_project.h"
|
||||
"fl_engine.h"
|
||||
"fl_json_message_codec.h"
|
||||
"fl_json_method_codec.h"
|
||||
"fl_message_codec.h"
|
||||
"fl_method_call.h"
|
||||
"fl_method_channel.h"
|
||||
"fl_method_codec.h"
|
||||
"fl_method_response.h"
|
||||
"fl_plugin_registrar.h"
|
||||
"fl_plugin_registry.h"
|
||||
"fl_standard_message_codec.h"
|
||||
"fl_standard_method_codec.h"
|
||||
"fl_string_codec.h"
|
||||
"fl_value.h"
|
||||
"fl_view.h"
|
||||
"flutter_linux.h"
|
||||
)
|
||||
list_prepend(FLUTTER_LIBRARY_HEADERS "${EPHEMERAL_DIR}/flutter_linux/")
|
||||
add_library(flutter INTERFACE)
|
||||
target_include_directories(flutter INTERFACE
|
||||
"${EPHEMERAL_DIR}"
|
||||
)
|
||||
target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}")
|
||||
target_link_libraries(flutter INTERFACE
|
||||
PkgConfig::GTK
|
||||
PkgConfig::GLIB
|
||||
PkgConfig::GIO
|
||||
)
|
||||
add_dependencies(flutter flutter_assemble)
|
||||
|
||||
# === Flutter tool backend ===
|
||||
# _phony_ is a non-existent file to force this command to run every time,
|
||||
# since currently there's no way to get a full input/output list from the
|
||||
# flutter tool.
|
||||
add_custom_command(
|
||||
OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS}
|
||||
${CMAKE_CURRENT_BINARY_DIR}/_phony_
|
||||
COMMAND ${CMAKE_COMMAND} -E env
|
||||
${FLUTTER_TOOL_ENVIRONMENT}
|
||||
"${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh"
|
||||
${FLUTTER_TARGET_PLATFORM} ${CMAKE_BUILD_TYPE}
|
||||
VERBATIM
|
||||
)
|
||||
add_custom_target(flutter_assemble DEPENDS
|
||||
"${FLUTTER_LIBRARY}"
|
||||
${FLUTTER_LIBRARY_HEADERS}
|
||||
)
|
||||
11
linux/flutter/generated_plugin_registrant.cc
Normal file
11
linux/flutter/generated_plugin_registrant.cc
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
//
|
||||
// Generated file. Do not edit.
|
||||
//
|
||||
|
||||
// clang-format off
|
||||
|
||||
#include "generated_plugin_registrant.h"
|
||||
|
||||
|
||||
void fl_register_plugins(FlPluginRegistry* registry) {
|
||||
}
|
||||
15
linux/flutter/generated_plugin_registrant.h
Normal file
15
linux/flutter/generated_plugin_registrant.h
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
//
|
||||
// Generated file. Do not edit.
|
||||
//
|
||||
|
||||
// clang-format off
|
||||
|
||||
#ifndef GENERATED_PLUGIN_REGISTRANT_
|
||||
#define GENERATED_PLUGIN_REGISTRANT_
|
||||
|
||||
#include <flutter_linux/flutter_linux.h>
|
||||
|
||||
// Registers Flutter plugins.
|
||||
void fl_register_plugins(FlPluginRegistry* registry);
|
||||
|
||||
#endif // GENERATED_PLUGIN_REGISTRANT_
|
||||
23
linux/flutter/generated_plugins.cmake
Normal file
23
linux/flutter/generated_plugins.cmake
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
#
|
||||
# Generated file, do not edit.
|
||||
#
|
||||
|
||||
list(APPEND FLUTTER_PLUGIN_LIST
|
||||
)
|
||||
|
||||
list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
||||
)
|
||||
|
||||
set(PLUGIN_BUNDLED_LIBRARIES)
|
||||
|
||||
foreach(plugin ${FLUTTER_PLUGIN_LIST})
|
||||
add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin})
|
||||
target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin)
|
||||
list(APPEND PLUGIN_BUNDLED_LIBRARIES $<TARGET_FILE:${plugin}_plugin>)
|
||||
list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries})
|
||||
endforeach(plugin)
|
||||
|
||||
foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST})
|
||||
add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin})
|
||||
list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries})
|
||||
endforeach(ffi_plugin)
|
||||
26
linux/runner/CMakeLists.txt
Normal file
26
linux/runner/CMakeLists.txt
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
cmake_minimum_required(VERSION 3.13)
|
||||
project(runner LANGUAGES CXX)
|
||||
|
||||
# Define the application target. To change its name, change BINARY_NAME in the
|
||||
# top-level CMakeLists.txt, not the value here, or `flutter run` will no longer
|
||||
# work.
|
||||
#
|
||||
# Any new source files that you add to the application should be added here.
|
||||
add_executable(${BINARY_NAME}
|
||||
"main.cc"
|
||||
"my_application.cc"
|
||||
"${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc"
|
||||
)
|
||||
|
||||
# Apply the standard set of build settings. This can be removed for applications
|
||||
# that need different build settings.
|
||||
apply_standard_settings(${BINARY_NAME})
|
||||
|
||||
# Add preprocessor definitions for the application ID.
|
||||
add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}")
|
||||
|
||||
# Add dependency libraries. Add any application-specific dependencies here.
|
||||
target_link_libraries(${BINARY_NAME} PRIVATE flutter)
|
||||
target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK)
|
||||
|
||||
target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}")
|
||||
6
linux/runner/main.cc
Normal file
6
linux/runner/main.cc
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
#include "my_application.h"
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
g_autoptr(MyApplication) app = my_application_new();
|
||||
return g_application_run(G_APPLICATION(app), argc, argv);
|
||||
}
|
||||
148
linux/runner/my_application.cc
Normal file
148
linux/runner/my_application.cc
Normal file
|
|
@ -0,0 +1,148 @@
|
|||
#include "my_application.h"
|
||||
|
||||
#include <flutter_linux/flutter_linux.h>
|
||||
#ifdef GDK_WINDOWING_X11
|
||||
#include <gdk/gdkx.h>
|
||||
#endif
|
||||
|
||||
#include "flutter/generated_plugin_registrant.h"
|
||||
|
||||
struct _MyApplication {
|
||||
GtkApplication parent_instance;
|
||||
char** dart_entrypoint_arguments;
|
||||
};
|
||||
|
||||
G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION)
|
||||
|
||||
// Called when first Flutter frame received.
|
||||
static void first_frame_cb(MyApplication* self, FlView* view) {
|
||||
gtk_widget_show(gtk_widget_get_toplevel(GTK_WIDGET(view)));
|
||||
}
|
||||
|
||||
// Implements GApplication::activate.
|
||||
static void my_application_activate(GApplication* application) {
|
||||
MyApplication* self = MY_APPLICATION(application);
|
||||
GtkWindow* window =
|
||||
GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application)));
|
||||
|
||||
// Use a header bar when running in GNOME as this is the common style used
|
||||
// by applications and is the setup most users will be using (e.g. Ubuntu
|
||||
// desktop).
|
||||
// If running on X and not using GNOME then just use a traditional title bar
|
||||
// in case the window manager does more exotic layout, e.g. tiling.
|
||||
// If running on Wayland assume the header bar will work (may need changing
|
||||
// if future cases occur).
|
||||
gboolean use_header_bar = TRUE;
|
||||
#ifdef GDK_WINDOWING_X11
|
||||
GdkScreen* screen = gtk_window_get_screen(window);
|
||||
if (GDK_IS_X11_SCREEN(screen)) {
|
||||
const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen);
|
||||
if (g_strcmp0(wm_name, "GNOME Shell") != 0) {
|
||||
use_header_bar = FALSE;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
if (use_header_bar) {
|
||||
GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new());
|
||||
gtk_widget_show(GTK_WIDGET(header_bar));
|
||||
gtk_header_bar_set_title(header_bar, "layer_wizard");
|
||||
gtk_header_bar_set_show_close_button(header_bar, TRUE);
|
||||
gtk_window_set_titlebar(window, GTK_WIDGET(header_bar));
|
||||
} else {
|
||||
gtk_window_set_title(window, "layer_wizard");
|
||||
}
|
||||
|
||||
gtk_window_set_default_size(window, 1280, 720);
|
||||
|
||||
g_autoptr(FlDartProject) project = fl_dart_project_new();
|
||||
fl_dart_project_set_dart_entrypoint_arguments(
|
||||
project, self->dart_entrypoint_arguments);
|
||||
|
||||
FlView* view = fl_view_new(project);
|
||||
GdkRGBA background_color;
|
||||
// Background defaults to black, override it here if necessary, e.g. #00000000
|
||||
// for transparent.
|
||||
gdk_rgba_parse(&background_color, "#000000");
|
||||
fl_view_set_background_color(view, &background_color);
|
||||
gtk_widget_show(GTK_WIDGET(view));
|
||||
gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view));
|
||||
|
||||
// Show the window when Flutter renders.
|
||||
// Requires the view to be realized so we can start rendering.
|
||||
g_signal_connect_swapped(view, "first-frame", G_CALLBACK(first_frame_cb),
|
||||
self);
|
||||
gtk_widget_realize(GTK_WIDGET(view));
|
||||
|
||||
fl_register_plugins(FL_PLUGIN_REGISTRY(view));
|
||||
|
||||
gtk_widget_grab_focus(GTK_WIDGET(view));
|
||||
}
|
||||
|
||||
// Implements GApplication::local_command_line.
|
||||
static gboolean my_application_local_command_line(GApplication* application,
|
||||
gchar*** arguments,
|
||||
int* exit_status) {
|
||||
MyApplication* self = MY_APPLICATION(application);
|
||||
// Strip out the first argument as it is the binary name.
|
||||
self->dart_entrypoint_arguments = g_strdupv(*arguments + 1);
|
||||
|
||||
g_autoptr(GError) error = nullptr;
|
||||
if (!g_application_register(application, nullptr, &error)) {
|
||||
g_warning("Failed to register: %s", error->message);
|
||||
*exit_status = 1;
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
g_application_activate(application);
|
||||
*exit_status = 0;
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
// Implements GApplication::startup.
|
||||
static void my_application_startup(GApplication* application) {
|
||||
// MyApplication* self = MY_APPLICATION(object);
|
||||
|
||||
// Perform any actions required at application startup.
|
||||
|
||||
G_APPLICATION_CLASS(my_application_parent_class)->startup(application);
|
||||
}
|
||||
|
||||
// Implements GApplication::shutdown.
|
||||
static void my_application_shutdown(GApplication* application) {
|
||||
// MyApplication* self = MY_APPLICATION(object);
|
||||
|
||||
// Perform any actions required at application shutdown.
|
||||
|
||||
G_APPLICATION_CLASS(my_application_parent_class)->shutdown(application);
|
||||
}
|
||||
|
||||
// Implements GObject::dispose.
|
||||
static void my_application_dispose(GObject* object) {
|
||||
MyApplication* self = MY_APPLICATION(object);
|
||||
g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev);
|
||||
G_OBJECT_CLASS(my_application_parent_class)->dispose(object);
|
||||
}
|
||||
|
||||
static void my_application_class_init(MyApplicationClass* klass) {
|
||||
G_APPLICATION_CLASS(klass)->activate = my_application_activate;
|
||||
G_APPLICATION_CLASS(klass)->local_command_line =
|
||||
my_application_local_command_line;
|
||||
G_APPLICATION_CLASS(klass)->startup = my_application_startup;
|
||||
G_APPLICATION_CLASS(klass)->shutdown = my_application_shutdown;
|
||||
G_OBJECT_CLASS(klass)->dispose = my_application_dispose;
|
||||
}
|
||||
|
||||
static void my_application_init(MyApplication* self) {}
|
||||
|
||||
MyApplication* my_application_new() {
|
||||
// Set the program name to the application ID, which helps various systems
|
||||
// like GTK and desktop environments map this running application to its
|
||||
// corresponding .desktop file. This ensures better integration by allowing
|
||||
// the application to be recognized beyond its binary name.
|
||||
g_set_prgname(APPLICATION_ID);
|
||||
|
||||
return MY_APPLICATION(g_object_new(my_application_get_type(),
|
||||
"application-id", APPLICATION_ID, "flags",
|
||||
G_APPLICATION_NON_UNIQUE, nullptr));
|
||||
}
|
||||
21
linux/runner/my_application.h
Normal file
21
linux/runner/my_application.h
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
#ifndef FLUTTER_MY_APPLICATION_H_
|
||||
#define FLUTTER_MY_APPLICATION_H_
|
||||
|
||||
#include <gtk/gtk.h>
|
||||
|
||||
G_DECLARE_FINAL_TYPE(MyApplication,
|
||||
my_application,
|
||||
MY,
|
||||
APPLICATION,
|
||||
GtkApplication)
|
||||
|
||||
/**
|
||||
* my_application_new:
|
||||
*
|
||||
* Creates a new Flutter-based application.
|
||||
*
|
||||
* Returns: a new #MyApplication.
|
||||
*/
|
||||
MyApplication* my_application_new();
|
||||
|
||||
#endif // FLUTTER_MY_APPLICATION_H_
|
||||
213
pubspec.lock
Normal file
213
pubspec.lock
Normal file
|
|
@ -0,0 +1,213 @@
|
|||
# Generated by pub
|
||||
# See https://dart.dev/tools/pub/glossary#lockfile
|
||||
packages:
|
||||
async:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: async
|
||||
sha256: e2eb0491ba5ddb6177742d2da23904574082139b07c1e33b8503b9f46f3e1a37
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.13.1"
|
||||
boolean_selector:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: boolean_selector
|
||||
sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.2"
|
||||
characters:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: characters
|
||||
sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.4.1"
|
||||
clock:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: clock
|
||||
sha256: e51d50bca3217c9a9fa2b41a30e4a38971133f5f9ec7a3d57bae095007f1d28e
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.3"
|
||||
collection:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: collection
|
||||
sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.19.1"
|
||||
cupertino_icons:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: cupertino_icons
|
||||
sha256: "41e005c33bd814be4d3096aff55b1908d419fde52ca656c8c47719ec745873cd"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.9"
|
||||
fake_async:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: fake_async
|
||||
sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.3.3"
|
||||
flutter:
|
||||
dependency: "direct main"
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
flutter_lints:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: flutter_lints
|
||||
sha256: "3105dc8492f6183fb076ccf1f351ac3d60564bff92e20bfc4af9cc1651f4e7e1"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.0.0"
|
||||
flutter_test:
|
||||
dependency: "direct dev"
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
leak_tracker:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: leak_tracker
|
||||
sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "11.0.2"
|
||||
leak_tracker_flutter_testing:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: leak_tracker_flutter_testing
|
||||
sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.10"
|
||||
leak_tracker_testing:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: leak_tracker_testing
|
||||
sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.2"
|
||||
lints:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: lints
|
||||
sha256: "12f842a479589fea194fe5c5a3095abc7be0c1f2ddfa9a0e76aed1dbd26a87df"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.1.0"
|
||||
matcher:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: matcher
|
||||
sha256: "31bd099b47c10cd1aeb55146a2d46ce0277630ecef3f7dae54ad7873f36696cd"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.12.20"
|
||||
material_color_utilities:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: material_color_utilities
|
||||
sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.13.0"
|
||||
meta:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: meta
|
||||
sha256: c82594181e3312f3d0695fc95aaaf7758d75b8d4ae2bbecf223b9fd5109a059d
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.18.3"
|
||||
path:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path
|
||||
sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.9.1"
|
||||
sky_engine:
|
||||
dependency: transitive
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
source_span:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: source_span
|
||||
sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.10.2"
|
||||
stack_trace:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: stack_trace
|
||||
sha256: "277654b3034d17ac6f9f1cb5595db011b1d5d41e8806866db28e0abaa101c490"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.12.2"
|
||||
stream_channel:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: stream_channel
|
||||
sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.4"
|
||||
string_scanner:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: string_scanner
|
||||
sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.4.1"
|
||||
term_glyph:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: term_glyph
|
||||
sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.2"
|
||||
test_api:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: test_api
|
||||
sha256: "2a122cbe059f8b610d3a5415f42e255b6c17b1f21eee1d960f31080237fb4f11"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.7.12"
|
||||
vector_math:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: vector_math
|
||||
sha256: "1d774bbdf6b72a0b12122fc1560c9c2d2a67db5a4a4cc2bd8a5c990ab20e3188"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.0"
|
||||
vm_service:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: vm_service
|
||||
sha256: "5f37239c4851efcef929cea7824e76df7f2f0970aef85d66bbc430afa40e72f0"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "15.3.0"
|
||||
sdks:
|
||||
dart: ">=3.13.5 <4.0.0"
|
||||
flutter: ">=3.18.0-18.0.pre.54"
|
||||
89
pubspec.yaml
Normal file
89
pubspec.yaml
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
name: layer_wizard
|
||||
description: "A new Flutter project."
|
||||
# The following line prevents the package from being accidentally published to
|
||||
# pub.dev using `flutter pub publish`. This is preferred for private packages.
|
||||
publish_to: 'none' # Remove this line if you wish to publish to pub.dev
|
||||
|
||||
# The following defines the version and build number for your application.
|
||||
# A version number is three numbers separated by dots, like 1.2.43
|
||||
# followed by an optional build number separated by a +.
|
||||
# Both the version and the builder number may be overridden in flutter
|
||||
# build by specifying --build-name and --build-number, respectively.
|
||||
# In Android, build-name is used as versionName while build-number used as versionCode.
|
||||
# Read more about Android versioning at https://developer.android.com/studio/publish/versioning
|
||||
# In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion.
|
||||
# Read more about iOS versioning at
|
||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||
# In Windows, build-name is used as the major, minor, and patch parts
|
||||
# of the product and file versions while build-number is used as the build suffix.
|
||||
version: 1.0.0+1
|
||||
|
||||
environment:
|
||||
sdk: ^3.13.5
|
||||
|
||||
# Dependencies specify other packages that your package needs in order to work.
|
||||
# To automatically upgrade your package dependencies to the latest versions
|
||||
# consider running `flutter pub upgrade --major-versions`. Alternatively,
|
||||
# dependencies can be manually updated by changing the version numbers below to
|
||||
# the latest version available on pub.dev. To see which dependencies have newer
|
||||
# versions available, run `flutter pub outdated`.
|
||||
dependencies:
|
||||
flutter:
|
||||
sdk: flutter
|
||||
|
||||
# The following adds the Cupertino Icons font to your application.
|
||||
# Use with the CupertinoIcons class for iOS style icons.
|
||||
cupertino_icons: ^1.0.8
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
|
||||
# The "flutter_lints" package below contains a set of recommended lints to
|
||||
# encourage good coding practices. The lint set provided by the package is
|
||||
# activated in the `analysis_options.yaml` file located at the root of your
|
||||
# package. See that file for information about deactivating specific lint
|
||||
# rules and activating additional ones.
|
||||
flutter_lints: ^6.0.0
|
||||
|
||||
# For information on the generic Dart part of this file, see the
|
||||
# following page: https://dart.dev/tools/pub/pubspec
|
||||
|
||||
# The following section is specific to Flutter packages.
|
||||
flutter:
|
||||
|
||||
# The following line ensures that the Material Icons font is
|
||||
# included with your application, so that you can use the icons in
|
||||
# the material Icons class.
|
||||
uses-material-design: true
|
||||
|
||||
# To add assets to your application, add an assets section, like this:
|
||||
# assets:
|
||||
# - images/a_dot_burr.jpeg
|
||||
# - images/a_dot_ham.jpeg
|
||||
|
||||
# An image asset can refer to one or more resolution-specific "variants", see
|
||||
# https://flutter.dev/to/resolution-aware-images
|
||||
|
||||
# For details regarding adding assets from package dependencies, see
|
||||
# https://flutter.dev/to/asset-from-package
|
||||
|
||||
# To add custom fonts to your application, add a fonts section here,
|
||||
# in this "flutter" section. Each entry in this list should have a
|
||||
# "family" key with the font family name, and a "fonts" key with a
|
||||
# list giving the asset and other descriptors for the font. For
|
||||
# example:
|
||||
# fonts:
|
||||
# - family: Schyler
|
||||
# fonts:
|
||||
# - asset: fonts/Schyler-Regular.ttf
|
||||
# - asset: fonts/Schyler-Italic.ttf
|
||||
# style: italic
|
||||
# - family: Trajan Pro
|
||||
# fonts:
|
||||
# - asset: fonts/TrajanPro.ttf
|
||||
# - asset: fonts/TrajanPro_Bold.ttf
|
||||
# weight: 700
|
||||
#
|
||||
# For details regarding fonts from package dependencies,
|
||||
# see https://flutter.dev/to/font-from-package
|
||||
17
test/widget_test.dart
Normal file
17
test/widget_test.dart
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import 'package:layer_wizard/main.dart';
|
||||
|
||||
void main() {
|
||||
testWidgets('app boots without crashing', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(const LayerWizardApp());
|
||||
// Intentionally a single pump, not pumpAndSettle: startup kicks off real
|
||||
// async work (reading a file, running `rpm-ostree status`) behind a
|
||||
// CircularProgressIndicator, which never "settles". This just checks
|
||||
// the app renders its initial loading frame without throwing.
|
||||
await tester.pump();
|
||||
|
||||
expect(find.byType(CircularProgressIndicator), findsOneWidget);
|
||||
});
|
||||
}
|
||||
Loading…
Reference in a new issue