Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,8 @@ jobs:
path: build-cov/coverage_html/
retention-days: 14
- uses: romeovs/lcov-reporter-action@v0.3.1
# never fail the job on the comment step
continue-on-error: true
with:
lcov-file: build-cov/coverage.info
github-token: ${{ secrets.GITHUB_TOKEN }}
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ build*/
.idea/
*.iml
.vscode/
.cache/

# CMake
CMakeCache.txt
Expand Down
39 changes: 39 additions & 0 deletions .pubignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# Files excluded from the pub.dev archive but kept in git.
# When this file is present, pub uses it instead of falling back to
# .gitignore, so every build/dev artifact must be re-listed here.

# Build artifacts (any out-of-source CMake build dir)
build/
build-*/
.cache/
*.o
*.a
*.so

# CMake scratch
CMakeCache.txt
CMakeFiles/
cmake_install.cmake
compile_commands.json

# Coverage
coverage/
coverage_html/
*.gcno
*.gcda
lcov.info

# Flutter example sub-package — has its own pubspec and hundreds of files
# (linux/ runner, generated plugin registrant); not needed by consumers.
example/flutter_remote_manager/

# IDE / editor / OS junk
.idea/
*.iml
.vscode/
*.swp
.DS_Store
Thumbs.db

# Claude Code state
.claude/
23 changes: 23 additions & 0 deletions example/launch_app.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// launch_app.dart — launch an installed app, then stop it.
// Run as: dart run example/launch_app.dart [app_id]
// Defaults to org.gnome.Calculator.

import 'package:flatpak_dart/flatpak_dart.dart';

Future<void> main(List<String> args) async {
final appId = args.isNotEmpty ? args.first : 'org.gnome.Calculator';
final client = FlatpakClient.user();

print('Launching $appId ...');
final instance = await client.launch(appId);
print('instance=${instance.instanceId} pid=${instance.pid} '
'childPid=${instance.childPid}');

print('\nStopping $appId ...');
await client.stop(appId);
print('Stopped (SIGTERM sent; SIGKILL follows after a 1.5s grace period)');

await Future<void>.delayed(const Duration(seconds: 2));

await client.close();
}
22 changes: 22 additions & 0 deletions example/list_running.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// list_running.dart — show currently running Flatpak app instances.
// Run as: dart run example/list_running.dart

import 'package:flatpak_dart/flatpak_dart.dart';

Future<void> main() async {
final client = FlatpakClient.user();
final running = await client.listRunning();

if (running.isEmpty) {
print('No running instances.');
} else {
print('${running.length} running instance(s):');
for (final i in running) {
print(' ${i.appId.padRight(36)} '
'instance=${i.instanceId} pid=${i.pid} '
'child=${i.childPid} running=${i.isRunning}');
}
}

await client.close();
}
148 changes: 80 additions & 68 deletions hook/build.dart
Original file line number Diff line number Diff line change
@@ -1,97 +1,109 @@
// hook/build.dart — Native asset build hook for flatpak_dart.
//
// Automatically builds libflatpak_nc.so via CMake when the package is
// consumed as a dependency. Requires: cmake, clang (or gcc), pkg-config,
// libflatpak-dev, libglib2.0-dev.

import 'dart:io';

import 'package:code_assets/code_assets.dart';
import 'package:hooks/hooks.dart';

void main(List<String> args) async {
await build(args, (input, output) async {
final packageRoot = input.packageRoot;
final nativeDir = packageRoot.resolve('native/');
final buildDir = input.outputDirectory.resolve('native_build/');
final buildDirPath = buildDir.toFilePath();

// Ensure build directory exists
await Directory(buildDirPath).create(recursive: true);

// Find a C++ compiler
final cxx = _findCompiler(['clang++-19', 'clang++', 'g++']);
final cc = _findCompiler(['clang-19', 'clang', 'gcc']);

// Run CMake configure
final configResult = await Process.run('cmake', [
'-B',
buildDirPath,
'-S',
nativeDir.toFilePath(),
'-DCMAKE_BUILD_TYPE=Release',
if (cxx != null) '-DCMAKE_CXX_COMPILER=$cxx',
if (cc != null) '-DCMAKE_C_COMPILER=$cc',
]);
if (configResult.exitCode != 0) {
throw Exception('CMake configure failed:\n${configResult.stderr}');
if (!input.config.buildCodeAssets) return;

// Allow an embedder/build system
final skipDefine = input.userDefines['skip_native_build'];
if (skipDefine == true || skipDefine == 'true') {
stderr.writeln(
'skip_native_build user-define set — skipping native build.',
);
return;
}

// Run CMake build
final cpuCount = Platform.numberOfProcessors;
final buildResult = await Process.run('cmake', [
final clearDefine = input.userDefines['clear_ambient_flags'];
final clearAmbientFlags = clearDefine == true || clearDefine == 'true';

final nativeDir = input.packageRoot.resolve('native/').toFilePath();
final buildDir =
input.outputDirectory.resolve('native_build/').toFilePath();

await Directory(buildDir).create(recursive: true);

final hasNinja = await _which('ninja');

if (!File('${buildDir}CMakeCache.txt').existsSync()) {
await _run('cmake', [
'-S',
nativeDir,
'-B',
buildDir,
'-DCMAKE_BUILD_TYPE=Release',
if (hasNinja) ...['-G', 'Ninja'],
], clearAmbientFlags: clearAmbientFlags);
}

await _run('cmake', [
'--build',
buildDirPath,
buildDir,
'--parallel',
'$cpuCount',
]);
if (buildResult.exitCode != 0) {
throw Exception('CMake build failed:\n${buildResult.stderr}');
}
], clearAmbientFlags: clearAmbientFlags);

// Add source files as dependencies for rebuild detection
output.dependencies.add(nativeDir.resolve('CMakeLists.txt'));
for (final uri in await _globSources(nativeDir.resolve('src/'))) {
output.dependencies.add(uri);
}
for (final uri in await _globSources(nativeDir.resolve('include/'))) {
output.dependencies.add(uri);
final libFile = File('${buildDir}libflatpak_nc.so');
if (!libFile.existsSync()) {
throw StateError('libflatpak_nc.so not found at ${libFile.path}');
}

// Register the built shared library as a code asset
final soPath = buildDir.resolve('libflatpak_nc.so');
output.assets.code.add(
CodeAsset(
package: input.packageName,
name: 'libflatpak_nc.so',
file: soPath,
linkMode: DynamicLoadingBundled(),
file: libFile.uri,
),
);

// Re-run the hook whenever any C/C++ source or CMake file changes.
for (final dir in ['src', 'include']) {
final d = Directory('$nativeDir$dir');
if (!d.existsSync()) continue;
for (final entity in d.listSync(recursive: true)) {
if (entity is! File) continue;
final p = entity.path;
if (p.endsWith('.cpp') ||
p.endsWith('.cc') ||
p.endsWith('.c') ||
p.endsWith('.hpp') ||
p.endsWith('.h')) {
output.dependencies.add(entity.uri);
}
}
}
output.dependencies.add(Uri.file('${nativeDir}CMakeLists.txt'));

stderr.writeln('libflatpak_nc built: ${libFile.path}');
});
}

String? _findCompiler(List<String> candidates) {
for (final name in candidates) {
final result = Process.runSync('sh', ['-c', 'command -v $name']);
if (result.exitCode == 0) {
return (result.stdout as String).trim();
}
const _clearedFlagVars = {'CFLAGS': '', 'CXXFLAGS': '', 'LDFLAGS': ''};

Future<void> _run(
String exe,
List<String> args, {
required bool clearAmbientFlags,
}) async {
final p = await Process.start(
exe,
args,
mode: ProcessStartMode.inheritStdio,
environment: clearAmbientFlags ? _clearedFlagVars : null,
);
final code = await p.exitCode;
if (code != 0) {
throw ProcessException(exe, args, 'exit code $code', code);
}
return null;
}

Future<List<Uri>> _globSources(Uri dir) async {
final directory = Directory.fromUri(dir);
if (!await directory.exists()) return [];
final files = <Uri>[];
await for (final entity in directory.list(recursive: true)) {
if (entity is File) {
final path = entity.path;
if (path.endsWith('.cpp') || path.endsWith('.c') || path.endsWith('.h')) {
files.add(entity.uri);
}
}
Future<bool> _which(String exe) async {
try {
final r = await Process.run('sh', ['-c', 'command -v $exe']);
return r.exitCode == 0;
} catch (_) {
return false;
}
return files;
}
1 change: 1 addition & 0 deletions lib/flatpak_dart.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ library;
export 'src/application.dart';
export 'src/exceptions.dart';
export 'src/flatpak_client.dart';
export 'src/instance.dart';
export 'src/known_remotes.dart';
export 'src/permissions.dart';
export 'src/remote.dart';
Expand Down
11 changes: 11 additions & 0 deletions lib/src/exceptions.dart
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,14 @@ final class FlatpakNotFoundException extends FlatpakException {
final class FlatpakRemoteException extends FlatpakException {
const FlatpakRemoteException(super.message);
}

/// Launching an application failed.
final class FlatpakLaunchException extends FlatpakException {
const FlatpakLaunchException(super.message);
}

/// Stopping an application failed. Distinct from [FlatpakNotFoundException]:
/// running instances *were* matched, but none of them could be signalled.
final class FlatpakStopException extends FlatpakException {
const FlatpakStopException(super.message);
}
63 changes: 63 additions & 0 deletions lib/src/ffi/bindings.dart
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,35 @@ external void _readerFetchRemoteMetadata(
Pointer<Utf8> ref,
);

@Native<
Void Function(
Pointer<Void>,
Int64,
Pointer<Utf8>,
Pointer<Utf8>,
Pointer<Utf8>,
Pointer<Utf8>,
)
>(symbol: 'flatpak_reader_launch')
external void _readerLaunch(
Pointer<Void> handle,
int port,
Pointer<Utf8> appId,
Pointer<Utf8> arch,
Pointer<Utf8> branch,
Pointer<Utf8> commit,
);

@Native<Void Function(Pointer<Void>, Int64, Pointer<Utf8>)>(
symbol: 'flatpak_reader_stop',
)
external void _readerStop(Pointer<Void> handle, int port, Pointer<Utf8> appId);

@Native<Void Function(Pointer<Void>, Int64)>(
symbol: 'flatpak_reader_list_running',
)
external void _readerListRunning(Pointer<Void> handle, int port);

// ── Worker ──────────────────────────────────────────────────────────────────

@Native<Pointer<Void> Function(Pointer<Utf8>)>(symbol: 'flatpak_worker_create')
Expand Down Expand Up @@ -394,6 +423,40 @@ abstract final class FlatpakBindings {
}
}

static void readerLaunch(
Pointer<Void> handle,
int port,
String appId,
String arch,
String branch,
String commit,
) {
final id = appId.toNativeUtf8();
final a = arch.toNativeUtf8();
final b = branch.toNativeUtf8();
final c = commit.toNativeUtf8();
try {
_readerLaunch(handle, port, id, a, b, c);
} finally {
calloc.free(id);
calloc.free(a);
calloc.free(b);
calloc.free(c);
}
}

static void readerStop(Pointer<Void> handle, int port, String appId) {
final id = appId.toNativeUtf8();
try {
_readerStop(handle, port, id);
} finally {
calloc.free(id);
}
}

static void readerListRunning(Pointer<Void> handle, int port) =>
_readerListRunning(handle, port);

// ── Worker ─────────────────────────────────────────────────────────────
static Pointer<Void> workerCreate(String installation) {
_ensureInitialized();
Expand Down
Loading
Loading