diff --git a/.earthlyignore b/.earthlyignore new file mode 100644 index 0000000..9ceecb2 --- /dev/null +++ b/.earthlyignore @@ -0,0 +1,13 @@ +# SPDX-FileCopyrightText: 2026 FocusFlow contributors +# SPDX-License-Identifier: AGPL-3.0-only + +.dart_tool/ +.git/ +.idea/ +build/ +builds/ +coverage/ +doc/api/ +apps/focus_flow_flutter/.dart_tool/ +apps/focus_flow_flutter/.plugin_symlinks/ +apps/focus_flow_flutter/build/ diff --git a/.gitignore b/.gitignore index 7a9aeed..7920108 100644 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,13 @@ +# SPDX-FileCopyrightText: 2026 FocusFlow contributors +# SPDX-License-Identifier: AGPL-3.0-only + # Dart .dart_tool/ .packages pubspec.lock !apps/focus_flow_flutter/pubspec.lock build/ +builds/ coverage/ doc/api/ diff --git a/Earthfile b/Earthfile new file mode 100644 index 0000000..2eed8dc --- /dev/null +++ b/Earthfile @@ -0,0 +1,102 @@ +# SPDX-FileCopyrightText: 2026 FocusFlow contributors +# SPDX-License-Identifier: AGPL-3.0-only + +VERSION 0.8 + +# Earthly build pipeline for FocusFlow. +# +# The primary application artifact is the Flutter Linux desktop release bundle. +# The +build target packages that bundle into builds/focus_flow_linux_x64.zip +# and also exports the unzipped runnable bundle under builds/focus_flow_linux_x64. + +setup: + FROM ubuntu:24.04 + USER root + ENV DEBIAN_FRONTEND=noninteractive + ENV FLUTTER_VERSION=3.44.3 + ENV FLUTTER_HOME=/opt/flutter + ENV PATH=/opt/flutter/bin:/opt/flutter/bin/cache/dart-sdk/bin:$PATH + RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ + ca-certificates \ + clang \ + cmake \ + curl \ + dbus-x11 \ + git \ + libgtk-3-dev \ + liblzma-dev \ + libstdc++-12-dev \ + libglu1-mesa \ + ninja-build \ + pkg-config \ + unzip \ + xz-utils \ + xvfb \ + zip \ + && rm -rf /var/lib/apt/lists/* + RUN curl -fsSL "https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_${FLUTTER_VERSION}-stable.tar.xz" -o /tmp/flutter.tar.xz \ + && mkdir -p /opt \ + && tar -xJf /tmp/flutter.tar.xz -C /opt \ + && rm /tmp/flutter.tar.xz + WORKDIR /workspace + RUN git config --global --add safe.directory /opt/flutter + RUN flutter config --no-analytics + RUN flutter config --enable-linux-desktop + RUN flutter --version + +code: + FROM +setup + COPY . . + RUN dart pub get + RUN cd apps/focus_flow_flutter && flutter pub get + +format: + FROM +code + RUN dart format --set-exit-if-changed packages apps scripts tool test + +dart-analyze: + FROM +code + RUN dart analyze + +flutter-analyze: + FROM +code + RUN cd apps/focus_flow_flutter && flutter analyze + +reuse: + FROM +code + RUN dart run tool/check_reuse.dart + +docs: + FROM +code + RUN dart run tool/check_docs.dart --skip-dartdoc + +pre-commit: + FROM +code + RUN .githooks/pre-commit + +lint: + FROM +code + BUILD +format + BUILD +dart-analyze + BUILD +flutter-analyze + BUILD +reuse + BUILD +docs + BUILD +pre-commit + +build: + FROM +code + RUN cd apps/focus_flow_flutter && flutter build linux --release + RUN rm -rf builds \ + && mkdir -p builds/focus_flow_linux_x64 \ + && cp -R apps/focus_flow_flutter/build/linux/x64/release/bundle/. builds/focus_flow_linux_x64/ + RUN cd builds && zip -qr focus_flow_linux_x64.zip focus_flow_linux_x64 + SAVE ARTIFACT --keep-ts builds AS LOCAL builds + +run: + FROM +build + RUN test -x builds/focus_flow_linux_x64/focus_flow_flutter + RUN status=0; timeout 10s xvfb-run -a builds/focus_flow_linux_x64/focus_flow_flutter || status=$?; if [ "$status" = "124" ]; then exit 0; fi; exit "$status" + +all: + BUILD +lint + BUILD +build diff --git a/README.md b/README.md index 70dc3a8..59d2141 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,26 @@ + + + # ADHD Scheduler Workspace SQLite-first Dart workspace for the ADHD scheduler backend, persistence adapters, notifications, exports, backups, integration tests, and CI scripts. -## Test +## Local Workflow + +Set up the workspace and local SQLite path: + +```sh +scripts/bootstrap_dev.sh +``` + +Run the local app during development: + +```sh +scripts/dev.sh +``` + +Run the full local quality gate: ```sh scripts/test.sh @@ -12,18 +29,17 @@ scripts/test.sh The test gate runs analyzer, tests with coverage, combines LCOV output, and fails below 80% package `lib/` coverage. -## Development +Package a local desktop release with the host toolchain: ```sh -scripts/bootstrap_dev.sh -scripts/dev.sh +scripts/package_release.sh --platform current --output build/releases ``` -The shell scripts prepare the default SQLite path and delegate to the Dart -development runner. +Equivalent Dart entry points: ```sh dart run scripts/dev.dart +dart run scripts/build.dart --platform current --output build/releases ``` The dev script prints the SQLite path, starts `dart run build_runner watch`, and @@ -42,6 +58,70 @@ Optional flags: dart run scripts/dev.dart --device linux --sqlite /tmp/scheduler.sqlite ``` +## Earthly Workflow + +Earthly is the supported containerized build path going forward. It builds a +Linux desktop Flutter bundle and exports a runnable application under `builds/`. + +Install prerequisites on the host: + +- Earthly +- Docker or another Earthly-compatible container runtime + +Set up the Earthly build image: + +```sh +earthly +setup +``` + +Copy code and resolve Dart/Flutter dependencies inside Earthly: + +```sh +earthly +code +``` + +Run all style and metadata checks: + +```sh +earthly +lint +``` + +Build and package the runnable Linux desktop app: + +```sh +earthly +build +``` + +Run lint, build, and package in one command: + +```sh +earthly +all +``` + +Smoke-run the Earthly-built Linux desktop app under a virtual display: + +```sh +earthly +run +``` + +Earthly outputs: + +```sh +builds/focus_flow_linux_x64/ +builds/focus_flow_linux_x64.zip +``` + +Useful focused lint targets: + +```sh +earthly +format +earthly +dart-analyze +earthly +flutter-analyze +earthly +reuse +earthly +docs +earthly +pre-commit +``` + ## Flutter UI The provisional UI app lives outside the root Dart workspace at diff --git a/tool/check_docs.dart b/tool/check_docs.dart index 222b63a..a74da08 100644 --- a/tool/check_docs.dart +++ b/tool/check_docs.dart @@ -116,28 +116,73 @@ Future> _trackedDartFilesMissingDeclarationDocs() async { /// Returns tracked Dart files in stable path order. /// -/// Git is the source of truth so local build outputs, generated docs, and -/// editor scratch files do not affect the documentation gate. +/// Git is preferred so local build outputs, generated docs, and editor scratch +/// files do not affect the documentation gate. Container build contexts may omit +/// `.git`, so the tool falls back to a conservative walk over source folders. Future> _trackedDartFiles() async { final result = await Process.run( 'git', ['ls-files', '*.dart'], runInShell: Platform.isWindows, ); - if (result.exitCode != 0) { - throw ProcessException( - 'git', - ['ls-files', '*.dart'], - 'Unable to list tracked Dart files.', - result.exitCode, - ); + if (result.exitCode == 0) { + return (result.stdout as String) + .split('\n') + .where((path) => path.trim().isNotEmpty) + .toList(growable: false) + ..sort(); } - return (result.stdout as String) - .split('\n') - .where((path) => path.trim().isNotEmpty) - .toList(growable: false) - ..sort(); + return _walkDartFiles(); +} + +/// Walks source folders for Dart files when Git metadata is unavailable. +/// +/// This fallback supports Earthly and other copied build contexts while still +/// avoiding generated output and tool caches that are not hand-maintained docs. +List _walkDartFiles() { + final files = []; + for (final entity in Directory.current.listSync(recursive: true)) { + if (entity is! File) { + continue; + } + final path = entity.path.replaceAll('\\', '/').replaceFirst('./', ''); + if (!path.endsWith('.dart') || + !_isDartAuditScope(path) || + _isIgnoredGeneratedPath(path)) { + continue; + } + files.add(path); + } + return files..sort(); +} + +/// Reports whether [path] belongs to source folders covered by Dartdoc policy. +/// +/// The root package, packages, scripts, tools, tests, and Flutter app are the +/// source surfaces where contributors add hand-maintained Dart code. +bool _isDartAuditScope(String path) { + return path.startsWith('packages/') || + path.startsWith('apps/focus_flow_flutter/') || + path.startsWith('scripts/') || + path.startsWith('tool/') || + path.startsWith('test/'); +} + +/// Reports whether [path] belongs to generated output or local tooling caches. +/// +/// The fallback walker skips these paths because they are not source files and +/// usually are not present in Git's tracked file list. +bool _isIgnoredGeneratedPath(String path) { + return path.startsWith('.dart_tool/') || + path.startsWith('.git/') || + path.startsWith('build/') || + path.startsWith('builds/') || + path.startsWith('coverage/') || + path.startsWith('doc/api/') || + path.contains('/.dart_tool/') || + path.contains('/build/') || + path.contains('/.plugin_symlinks/'); } /// Finds tracked Dart files that are missing file-level library docs. diff --git a/tool/check_reuse.dart b/tool/check_reuse.dart index 84e3c23..1d6e2d7 100644 --- a/tool/check_reuse.dart +++ b/tool/check_reuse.dart @@ -137,27 +137,60 @@ List _spdxHeaderFailures() { /// Returns tracked repository files in stable path order. /// -/// Git is used so ignored build output and local editor files do not affect the -/// metadata check. +/// Git is preferred so ignored build output and local editor files do not affect +/// the metadata check. Container build contexts may omit `.git`, so the checker +/// falls back to a conservative tree walk over the compliance scope. List _trackedFiles() { final result = Process.runSync( 'git', ['ls-files'], runInShell: Platform.isWindows, ); - if (result.exitCode != 0) { - throw ProcessException( - 'git', - ['ls-files'], - 'Unable to list tracked files.', - result.exitCode, - ); + if (result.exitCode == 0) { + return (result.stdout as String) + .split('\n') + .where((path) => path.trim().isNotEmpty) + .toList(growable: false) + ..sort(); } - return (result.stdout as String) - .split('\n') - .where((path) => path.trim().isNotEmpty) - .toList(growable: false) - ..sort(); + + return _walkComplianceFiles(); +} + +/// Walks source-controlled compliance paths when Git metadata is unavailable. +/// +/// This path is used by Earthly and other copied build contexts. Generated +/// output and tool caches are skipped explicitly so stale local artifacts do not +/// create false failures. +List _walkComplianceFiles() { + final files = []; + for (final entity in Directory.current.listSync(recursive: true)) { + if (entity is! File) { + continue; + } + final path = entity.path.replaceAll('\\', '/').replaceFirst('./', ''); + if (_isIgnoredGeneratedPath(path) || !_isComplianceScope(path)) { + continue; + } + files.add(path); + } + return files..sort(); +} + +/// Reports whether [path] belongs to generated output or local tooling caches. +/// +/// These paths are intentionally ignored by the fallback scanner because they +/// are not source files and are normally excluded by Git. +bool _isIgnoredGeneratedPath(String path) { + return path.startsWith('.dart_tool/') || + path.startsWith('.git/') || + path.startsWith('build/') || + path.startsWith('builds/') || + path.startsWith('coverage/') || + path.startsWith('doc/api/') || + path.contains('/.dart_tool/') || + path.contains('/build/') || + path.contains('/.plugin_symlinks/'); } /// Reports whether [path] should carry an inline SPDX header. @@ -171,7 +204,10 @@ bool _requiresInlineSpdxHeader(String path) { if (path.endsWith('.g.dart') || path.endsWith('LICENSE.md')) { return false; } - return path.endsWith('.dart') || + return path == '.earthlyignore' || + path == '.gitignore' || + path == 'Earthfile' || + path.endsWith('.dart') || path.endsWith('.yaml') || path.endsWith('.yml') || path.endsWith('.sh') || @@ -187,8 +223,12 @@ bool _isComplianceScope(String path) { path.startsWith('apps/focus_flow_flutter/') || path.startsWith('scripts/') || path.startsWith('tool/') || + path == '.earthlyignore' || + path == '.gitignore' || path == 'AGENTS.md' || + path == 'Earthfile' || path == 'Focus Flow Contributors.md' || + path == 'README.md' || path == 'pubspec.yaml' || path == 'analysis_options.yaml' || path == '.github/workflows/ci.yml';