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