diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..769218c --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,605 @@ +name: Papyrus Build and Release + +on: + push: + branches: + - main + tags: + - 'v*' + pull_request: + branches: + - main + workflow_dispatch: + inputs: + tag: + description: 'Release tag, for example v0.1.0' + required: true + type: string + draft: + description: 'Create the GitHub Release as a draft' + required: false + default: false + type: boolean + +permissions: + contents: read + +concurrency: + group: papyrus-release-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + +env: + FLUTTER_VERSION: '3.41.9' + +jobs: + prepare: + name: Prepare release metadata + runs-on: ubuntu-latest + outputs: + should_release: ${{ steps.metadata.outputs.should_release }} + tag_name: ${{ steps.metadata.outputs.tag_name }} + is_draft: ${{ steps.metadata.outputs.is_draft }} + is_prerelease: ${{ steps.metadata.outputs.is_prerelease }} + build_name: ${{ steps.metadata.outputs.build_name }} + build_number: ${{ steps.metadata.outputs.build_number }} + artifact_version: ${{ steps.metadata.outputs.artifact_version }} + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Validate release metadata + id: metadata + shell: bash + env: + EVENT_NAME: ${{ github.event_name }} + REF_TYPE: ${{ github.ref_type }} + REF_NAME: ${{ github.ref_name }} + INPUT_TAG: ${{ github.event.inputs.tag }} + INPUT_DRAFT: ${{ github.event.inputs.draft }} + AUTO_RELEASE_DRAFT: ${{ vars.AUTO_RELEASE_DRAFT }} + RUN_NUMBER: ${{ github.run_number }} + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_REPO: ${{ github.repository }} + run: | + set -euo pipefail + + pubspec_version="$(awk '/^version:[[:space:]]*/ { print $2; exit }' pubspec.yaml)" + if [[ -z "$pubspec_version" ]]; then + echo "Unable to read version from pubspec.yaml" + exit 1 + fi + + pubspec_name="${pubspec_version%%+*}" + tag_name="" + tag_version="$pubspec_name" + build_name="$pubspec_name" + should_release=false + is_draft=false + + if [[ "$EVENT_NAME" == "workflow_dispatch" ]]; then + tag_name="$INPUT_TAG" + should_release=true + if [[ "$INPUT_DRAFT" == "true" ]]; then + is_draft=true + fi + elif [[ "$REF_TYPE" == "tag" && "$REF_NAME" == v* ]]; then + tag_name="$REF_NAME" + should_release=true + elif [[ "$EVENT_NAME" == "push" && "$REF_TYPE" == "branch" && "$REF_NAME" == "main" && "$AUTO_RELEASE_DRAFT" == "true" ]]; then + tag_name="v${pubspec_name}" + tag_ref="repos/${GH_REPO}/git/ref/tags/${tag_name}" + tag_lookup_output="" + + if tag_lookup_output="$(gh api "$tag_ref" --jq '.ref' 2>&1)"; then + echo "Remote tag $tag_name already exists; skipping automatic draft release." + elif [[ "$tag_lookup_output" == *"404"* || "$tag_lookup_output" == *"Not Found"* ]]; then + echo "Remote tag $tag_name does not exist; enabling automatic draft release." + should_release=true + is_draft=true + else + echo "Unable to inspect remote tag $tag_name:" >&2 + echo "$tag_lookup_output" >&2 + exit 1 + fi + fi + + if [[ "$should_release" == "true" ]]; then + if [[ ! "$tag_name" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$ ]]; then + echo "Release tag must match v: $tag_name" + exit 1 + fi + + tag_version="${tag_name#v}" + if [[ "$tag_version" != "$pubspec_name" ]]; then + echo "Release tag $tag_name does not match pubspec.yaml version $pubspec_version" + echo "The tag must match the version before the optional +build suffix." + exit 1 + fi + else + tag_name="snapshot-${GITHUB_SHA:0:7}" + fi + + build_name="${tag_version%%-*}" + + # Any semver prerelease suffix (hyphen after the core version) is a prerelease. + is_prerelease=false + if [[ "$tag_name" == *-* ]]; then + is_prerelease=true + fi + + { + echo "should_release=$should_release" + echo "tag_name=$tag_name" + echo "is_draft=$is_draft" + echo "is_prerelease=$is_prerelease" + echo "build_name=$build_name" + echo "build_number=$RUN_NUMBER" + echo "artifact_version=$tag_version" + } >> "$GITHUB_OUTPUT" + + quality: + name: Analyze and test + runs-on: ubuntu-latest + needs: prepare + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Flutter + uses: subosito/flutter-action@v2 + with: + channel: stable + flutter-version: ${{ env.FLUTTER_VERSION }} + cache: true + pub-cache: true + + - name: Install dependencies + run: flutter pub get + + - name: Regenerate Drift code + run: dart run build_runner build --delete-conflicting-outputs + + - name: Verify generated Drift code + run: git diff --exit-code -- lib/data/local/app_database.g.dart + + - name: Analyze Dart code + # Pre-existing info-level lints should not block CI; warnings/errors still fail. + run: flutter analyze --no-fatal-infos + + - name: Run tests + run: flutter test --coverage + + # Temporarily disabled: app entry still imports dart:io unconditionally + # (LogProvider, BackupService, etc.), so `flutter build web` fails. + # Re-enable after conditional imports / stubs land; set repo var ENABLE_WEB_BUILD=true. + web: + name: Build Web + if: ${{ vars.ENABLE_WEB_BUILD == 'true' }} + runs-on: ubuntu-latest + needs: [prepare, quality] + env: + ARTIFACT_VERSION: ${{ needs.prepare.outputs.artifact_version }} + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Flutter + uses: subosito/flutter-action@v2 + with: + channel: stable + flutter-version: ${{ env.FLUTTER_VERSION }} + cache: true + pub-cache: true + + - name: Enable Web + run: flutter config --enable-web + + - name: Install dependencies + run: flutter pub get + + - name: Build Web release + run: flutter build web --release + + - name: Package Web release + shell: bash + run: | + set -euo pipefail + test -d build/web + test -n "$(find build/web -type f -print -quit)" + mkdir -p artifacts + (cd build && zip -qr "../artifacts/Papyrus-web-${ARTIFACT_VERSION}.zip" web) + test -s "artifacts/Papyrus-web-${ARTIFACT_VERSION}.zip" + + - name: Upload Web package + uses: actions/upload-artifact@v4 + with: + name: package-web + path: artifacts/Papyrus-web-${{ needs.prepare.outputs.artifact_version }}.zip + if-no-files-found: error + retention-days: 7 + compression-level: 0 + + android: + name: Build Android + if: ${{ github.event_name != 'pull_request' }} + runs-on: ubuntu-latest + needs: [prepare, quality] + env: + BUILD_NAME: ${{ needs.prepare.outputs.build_name }} + BUILD_NUMBER: ${{ needs.prepare.outputs.build_number }} + ARTIFACT_VERSION: ${{ needs.prepare.outputs.artifact_version }} + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Java 17 + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: '17' + cache: gradle + + - name: Set up Flutter + uses: subosito/flutter-action@v2 + with: + channel: stable + flutter-version: ${{ env.FLUTTER_VERSION }} + cache: true + pub-cache: true + + - name: Install dependencies + run: flutter pub get + + - name: Build Android APK + run: flutter build apk --release --build-name "$BUILD_NAME" --build-number "$BUILD_NUMBER" + + - name: Build Android App Bundle + run: flutter build appbundle --release --build-name "$BUILD_NAME" --build-number "$BUILD_NUMBER" + + - name: Package Android releases + shell: bash + run: | + set -euo pipefail + test -s build/app/outputs/flutter-apk/app-release.apk + test -s build/app/outputs/bundle/release/app-release.aab + mkdir -p artifacts + cp build/app/outputs/flutter-apk/app-release.apk \ + "artifacts/Papyrus-android-${ARTIFACT_VERSION}.apk" + cp build/app/outputs/bundle/release/app-release.aab \ + "artifacts/Papyrus-android-${ARTIFACT_VERSION}.aab" + echo "Android release uses the repository debug signing configuration and is not Play Store-ready." + + - name: Upload Android packages + uses: actions/upload-artifact@v4 + with: + name: package-android + path: artifacts/Papyrus-android-${{ needs.prepare.outputs.artifact_version }}.* + if-no-files-found: error + retention-days: 7 + compression-level: 0 + + windows: + name: Build Windows + if: ${{ github.event_name != 'pull_request' }} + runs-on: windows-latest + needs: [prepare, quality] + env: + BUILD_NAME: ${{ needs.prepare.outputs.build_name }} + BUILD_NUMBER: ${{ needs.prepare.outputs.build_number }} + ARTIFACT_VERSION: ${{ needs.prepare.outputs.artifact_version }} + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Flutter + uses: subosito/flutter-action@v2 + with: + channel: stable + flutter-version: ${{ env.FLUTTER_VERSION }} + cache: true + pub-cache: true + + - name: Enable Windows desktop + run: flutter config --enable-windows-desktop + + - name: Install dependencies + run: flutter pub get + + - name: Build Windows release + run: flutter build windows --release --build-name $env:BUILD_NAME --build-number $env:BUILD_NUMBER + + - name: Package Windows release + shell: pwsh + run: | + $ErrorActionPreference = 'Stop' + $releaseDir = Join-Path $PWD 'build/windows/x64/runner/Release' + if (-not (Test-Path $releaseDir)) { throw "Missing Windows release directory: $releaseDir" } + if (-not (Get-ChildItem $releaseDir -File -Recurse)) { throw 'Windows release directory is empty' } + $artifactDir = Join-Path $PWD 'artifacts' + New-Item -ItemType Directory -Force -Path $artifactDir | Out-Null + $zipPath = Join-Path $artifactDir "Papyrus-windows-x64-$env:ARTIFACT_VERSION.zip" + Compress-Archive -Path $releaseDir -DestinationPath $zipPath -Force + if ((Get-Item $zipPath).Length -le 0) { throw 'Windows package is empty' } + + - name: Upload Windows package + uses: actions/upload-artifact@v4 + with: + name: package-windows + path: artifacts/Papyrus-windows-x64-${{ needs.prepare.outputs.artifact_version }}.zip + if-no-files-found: error + retention-days: 7 + compression-level: 0 + + linux: + name: Build Linux + if: ${{ github.event_name != 'pull_request' }} + runs-on: ubuntu-latest + needs: [prepare, quality] + env: + ARTIFACT_VERSION: ${{ needs.prepare.outputs.artifact_version }} + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Install Linux build dependencies + run: | + sudo apt-get update + sudo apt-get install -y clang cmake ninja-build pkg-config libgtk-3-dev liblzma-dev + + - name: Set up Flutter + uses: subosito/flutter-action@v2 + with: + channel: stable + flutter-version: ${{ env.FLUTTER_VERSION }} + cache: true + pub-cache: true + + - name: Enable Linux desktop + run: flutter config --enable-linux-desktop + + - name: Install dependencies + run: flutter pub get + + - name: Build Linux release + run: flutter build linux --release + + - name: Package Linux release + shell: bash + run: | + set -euo pipefail + bundle_dir=build/linux/x64/release/bundle + test -d "$bundle_dir" + test -n "$(find "$bundle_dir" -type f -print -quit)" + mkdir -p artifacts + tar -czf "artifacts/Papyrus-linux-x64-${ARTIFACT_VERSION}.tar.gz" \ + -C build/linux/x64/release bundle + test -s "artifacts/Papyrus-linux-x64-${ARTIFACT_VERSION}.tar.gz" + + - name: Upload Linux package + uses: actions/upload-artifact@v4 + with: + name: package-linux + path: artifacts/Papyrus-linux-x64-${{ needs.prepare.outputs.artifact_version }}.tar.gz + if-no-files-found: error + retention-days: 7 + compression-level: 0 + + macos: + name: Build macOS + if: ${{ github.event_name != 'pull_request' }} + runs-on: macos-latest + needs: [prepare, quality] + env: + BUILD_NAME: ${{ needs.prepare.outputs.build_name }} + BUILD_NUMBER: ${{ needs.prepare.outputs.build_number }} + ARTIFACT_VERSION: ${{ needs.prepare.outputs.artifact_version }} + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Flutter + uses: subosito/flutter-action@v2 + with: + channel: stable + flutter-version: ${{ env.FLUTTER_VERSION }} + cache: true + pub-cache: true + + - name: Enable macOS desktop + run: flutter config --enable-macos-desktop + + - name: Install dependencies + run: flutter pub get + + - name: Build macOS release + run: flutter build macos --release --no-codesign --build-name "$BUILD_NAME" --build-number "$BUILD_NUMBER" + + - name: Package macOS release + shell: bash + run: | + set -euo pipefail + app_path="$(find build/macos/Build/Products/Release -maxdepth 1 -type d -name '*.app' -print -quit)" + if [[ -z "$app_path" ]]; then + echo 'No macOS .app bundle was produced' + exit 1 + fi + mkdir -p artifacts + ditto -c -k --sequesterRsrc --keepParent "$app_path" \ + "artifacts/Papyrus-macos-${ARTIFACT_VERSION}.zip" + test -s "artifacts/Papyrus-macos-${ARTIFACT_VERSION}.zip" + + - name: Upload macOS package + uses: actions/upload-artifact@v4 + with: + name: package-macos + path: artifacts/Papyrus-macos-${{ needs.prepare.outputs.artifact_version }}.zip + if-no-files-found: error + retention-days: 7 + compression-level: 0 + + ios: + name: Validate iOS build + if: ${{ github.event_name != 'pull_request' }} + runs-on: macos-latest + needs: [prepare, quality] + env: + BUILD_NAME: ${{ needs.prepare.outputs.build_name }} + BUILD_NUMBER: ${{ needs.prepare.outputs.build_number }} + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Flutter + uses: subosito/flutter-action@v2 + with: + channel: stable + flutter-version: ${{ env.FLUTTER_VERSION }} + cache: true + pub-cache: true + + - name: Install dependencies + run: flutter pub get + + - name: Build iOS without code signing + run: flutter build ios --release --no-codesign --build-name "$BUILD_NAME" --build-number "$BUILD_NUMBER" + + - name: Verify iOS app bundle + shell: bash + run: | + set -euo pipefail + app_path="$(find build/ios/iphoneos -maxdepth 1 -type d -name '*.app' -print -quit)" + if [[ -z "$app_path" ]]; then + echo 'No iOS .app bundle was produced' + exit 1 + fi + echo "Validated unsigned iOS bundle: $app_path" + + publish: + name: Publish GitHub Release + runs-on: ubuntu-latest + needs: [prepare, quality, web, android, windows, linux, macos, ios] + if: ${{ always() && needs.prepare.outputs.should_release == 'true' && needs.quality.result == 'success' && (needs.web.result == 'success' || needs.web.result == 'skipped') && needs.android.result == 'success' && needs.windows.result == 'success' && needs.linux.result == 'success' && needs.macos.result == 'success' && needs.ios.result == 'success' }} + permissions: + contents: write + env: + ARTIFACT_VERSION: ${{ needs.prepare.outputs.artifact_version }} + TAG_NAME: ${{ needs.prepare.outputs.tag_name }} + IS_DRAFT: ${{ needs.prepare.outputs.is_draft }} + IS_PRERELEASE: ${{ needs.prepare.outputs.is_prerelease }} + WEB_BUILT: ${{ needs.web.result == 'success' }} + steps: + - name: Download platform packages + uses: actions/download-artifact@v4 + with: + pattern: package-* + path: release-assets + merge-multiple: true + + - name: Validate packages and create checksums + shell: bash + run: | + set -euo pipefail + + expected=( + "release-assets/Papyrus-windows-x64-${ARTIFACT_VERSION}.zip" + "release-assets/Papyrus-macos-${ARTIFACT_VERSION}.zip" + "release-assets/Papyrus-linux-x64-${ARTIFACT_VERSION}.tar.gz" + "release-assets/Papyrus-android-${ARTIFACT_VERSION}.apk" + "release-assets/Papyrus-android-${ARTIFACT_VERSION}.aab" + ) + + if [[ "$WEB_BUILT" == "true" ]]; then + expected+=("release-assets/Papyrus-web-${ARTIFACT_VERSION}.zip") + fi + + for file in "${expected[@]}"; do + if [[ ! -s "$file" ]]; then + echo "Missing or empty release asset: $file" + exit 1 + fi + done + + unzip -t "release-assets/Papyrus-windows-x64-${ARTIFACT_VERSION}.zip" >/dev/null + unzip -t "release-assets/Papyrus-macos-${ARTIFACT_VERSION}.zip" >/dev/null + tar -tzf "release-assets/Papyrus-linux-x64-${ARTIFACT_VERSION}.tar.gz" >/dev/null + if [[ "$WEB_BUILT" == "true" ]]; then + unzip -t "release-assets/Papyrus-web-${ARTIFACT_VERSION}.zip" >/dev/null + fi + + checksum_files=( + "Papyrus-windows-x64-${ARTIFACT_VERSION}.zip" + "Papyrus-macos-${ARTIFACT_VERSION}.zip" + "Papyrus-linux-x64-${ARTIFACT_VERSION}.tar.gz" + "Papyrus-android-${ARTIFACT_VERSION}.apk" + "Papyrus-android-${ARTIFACT_VERSION}.aab" + ) + if [[ "$WEB_BUILT" == "true" ]]; then + checksum_files+=("Papyrus-web-${ARTIFACT_VERSION}.zip") + fi + ( + cd release-assets + sha256sum "${checksum_files[@]}" + ) > release-assets/SHA256SUMS.txt + + - name: Create or update GitHub Release + shell: bash + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_REPO: ${{ github.repository }} + TARGET_SHA: ${{ github.sha }} + run: | + set -euo pipefail + + tag_ref="repos/${GH_REPO}/git/ref/tags/${TAG_NAME}" + tag_lookup_output="" + if ! tag_lookup_output="$(gh api "$tag_ref" --jq '.object.type' 2>&1)"; then + if [[ "$tag_lookup_output" == *"404"* || "$tag_lookup_output" == *"Not Found"* ]]; then + echo "Remote tag $TAG_NAME does not exist; it will be created at $TARGET_SHA." + else + echo "Unable to inspect remote tag $TAG_NAME:" >&2 + echo "$tag_lookup_output" >&2 + exit 1 + fi + else + tag_object_type="$tag_lookup_output" + tag_object_sha="$(gh api "$tag_ref" --jq '.object.sha')" + + while [[ "$tag_object_type" == "tag" ]]; do + tag_object_type="$(gh api "repos/${GH_REPO}/git/tags/${tag_object_sha}" --jq '.object.type')" + tag_object_sha="$(gh api "repos/${GH_REPO}/git/tags/${tag_object_sha}" --jq '.object.sha')" + done + + if [[ "$tag_object_type" != "commit" ]]; then + echo "Remote tag $TAG_NAME resolves to unsupported object type: $tag_object_type" >&2 + exit 1 + fi + + if [[ "$tag_object_sha" != "$TARGET_SHA" ]]; then + echo "Refusing to publish $TAG_NAME: remote tag resolves to $tag_object_sha, but this workflow targets $TARGET_SHA." >&2 + echo "The release tag must point to the exact commit that produced these artifacts." >&2 + exit 1 + fi + + echo "Verified $TAG_NAME points to $TARGET_SHA." + fi + + if gh release view "$TAG_NAME" >/dev/null 2>&1; then + gh release upload "$TAG_NAME" release-assets/* --clobber + gh release edit "$TAG_NAME" \ + --title "Papyrus $TAG_NAME" \ + --draft="$IS_DRAFT" \ + --prerelease="$IS_PRERELEASE" + else + release_flags=( + --title "Papyrus $TAG_NAME" + --generate-notes + --target "$TARGET_SHA" + ) + if [[ "$IS_DRAFT" == "true" ]]; then + release_flags+=(--draft) + fi + if [[ "$IS_PRERELEASE" == "true" ]]; then + release_flags+=(--prerelease) + fi + gh release create "$TAG_NAME" release-assets/* "${release_flags[@]}" + fi diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..2457acb --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,6 @@ +{ + "recommendations": [ + "Dart-Code.dart-code", + "Dart-Code.flutter" + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..92b7d76 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,18 @@ +{ + "editor.formatOnSave": true, + "editor.codeActionsOnSave": { + "source.fixAll": "explicit", + "source.organizeImports": "explicit" + }, + "[dart]": { + "editor.formatOnSave": true, + "editor.selectionHighlight": false, + "editor.suggestSelection": "first", + "editor.tabCompletion": "onlySnippets", + "editor.wordBasedSuggestions": "off" + }, + "files.exclude": { + "**/.dart_tool": true, + "**/.packages": true + } +} diff --git a/README.md b/README.md index a319120..ba03ffd 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,8 @@ 核心设计哲学:**极简、键盘驱动、流状态(Flow State)优先**。 +详细架构与开发规范见 [AGENTS.md](AGENTS.md)、[STRUCTURE.md](STRUCTURE.md)、[PRD.md](PRD.md)。 + --- ## 核心定位 @@ -35,14 +37,61 @@ | 项目 | 版本 / 选型 | |------|-------------| -| Flutter SDK | 3.41.x | +| Flutter SDK | 3.41.x(CI 固定 3.41.9;本地可使用更新的 stable) | | Dart SDK | 3.11.x | | UI | fluent_ui 4.15.x | | 状态管理 | Provider + ChangeNotifier | | 持久化 | Drift (SQLite),全平台含 Web WASM | | 目标平台 | Android / iOS / Windows / macOS / Linux / Web | -## 快速开始 +## 环境要求 + +| 组件 | 版本 | +|------|------| +| Flutter | 3.41.9(CI 固定此版本;本地可使用更新的 stable) | +| Dart | 3.11+ | +| Windows 桌面构建 | Visual Studio 2022 +「使用 C++ 的桌面开发」工作负载 | +| Android 构建 | Android Studio + Android SDK(可选) | +| Web 调试 | Chrome / Edge | + +## 快速开始(Windows) + +### 1. 安装 Flutter + +若尚未安装,可克隆到脚本默认的 `C:\src\flutter`: + +```powershell +git clone https://github.com/flutter/flutter.git -b stable --depth 1 C:\src\flutter +``` + +将 Flutter 的 `bin` 目录加入用户 PATH,然后**重新打开终端**。也可以先设置 `FLUTTER_ROOT`,让初始化脚本使用其他安装目录。 + +### 2. 一键初始化 + +```powershell +.\scripts\setup.ps1 +``` + +或手动执行: + +```powershell +flutter doctor +flutter pub get +dart run build_runner build +flutter test +``` + +### 3. 运行应用 + +```powershell +# Web(无需 Visual Studio) +flutter run -d chrome + +# Windows 桌面(需安装 Visual Studio) +flutter run -d windows +``` + +## 快速开始(其他平台) ```bash # 安装依赖 @@ -62,12 +111,36 @@ flutter run -d chrome flutter test ``` -数据库表结构变更后需重新生成 Drift 代码: +## IDE 配置 + +项目已包含 `.vscode/settings.json` 与 `extensions.json`,在 Cursor / VS Code 中打开项目后: + +1. 安装推荐扩展:**Dart**、**Flutter** +2. 确保 Flutter SDK 的 `bin` 目录已加入 PATH;项目不再把 SDK 路径硬编码到工作区设置中 + +## 数据库代码生成 + +修改 `lib/data/local/app_database.dart` 中的 Drift 表定义后: ```bash dart run build_runner build ``` +## 平台说明 + +- **Windows / Linux / Android**:使用 `sqlite3` 包内置的原生库(默认)。 +- **iOS / macOS**:若 GitHub 下载 sqlite3 预编译库失败,可在 `pubspec.yaml` 中临时添加 `hooks.user_defines.sqlite3.source: system` 使用系统 SQLite(参见 AGENTS.md)。**Windows 上请勿启用此配置。** +- **Web**:应用入口路径仍有无条件 `dart:io` 依赖,CI/Release 暂不构建 Web 产物;本地调试前需先完成条件导入改造。 + +## 常用命令 + +```bash +flutter analyze # 静态分析 +flutter test # 单元测试 +flutter build windows # 构建 Windows 发布版 +flutter build web # 构建 Web 版(当前可能因 dart:io 失败) +``` + ## 键盘快捷键(学习区) | 按键 | 动作 | @@ -90,8 +163,6 @@ lib/ └── logging/ # 日志系统 ``` -详细说明见 [`STRUCTURE.md`](STRUCTURE.md),产品需求见 [`PRD.md`](PRD.md),Agent 开发约定见 [`AGENTS.md`](AGENTS.md)。 - ## 批量导入格式 UTF-8 文本,空行分块,块内用 `===` 分隔题目与答案: @@ -102,6 +173,25 @@ UTF-8 文本,空行分块,块内用 `===` 分隔题目与答案: 题目 B === 答案 B ``` +## GitHub Actions 打包与发布 + +项目包含 `.github/workflows/release.yml`:Pull Request 会执行分析与测试;`main` 分支推送、`v*` Tag 推送或手动运行 workflow 时,才执行完整的平台构建。推送 `v*` Tag 或手动运行 workflow 时,所有平台构建成功后才会创建 GitHub Release。 + +发布 Tag 必须与 `pubspec.yaml` 中去掉 `+build` 部分的版本一致。例如 `version: 0.1.0+1` 对应: + +```bash +git tag v0.1.0 +git push origin v0.1.0 +``` + +也可以在 Actions 中手动运行 `Papyrus Build and Release`,填写 `tag` 并选择是否创建 Draft Release。 + +Release 会提供 Windows、macOS、Linux、Android APK 和 Android AAB 产物,以及 `SHA256SUMS.txt`。当前 Android Release 仍使用 debug signing,iOS 仅执行 `--no-codesign` 构建校验,macOS 产物未签名/未公证,均不代表商店发布包。Web 产物暂未纳入 Release(见上方平台说明)。 + +若仓库变量 `AUTO_RELEASE_DRAFT=true`,推送到 `main` 且对应 `v` Tag 尚不存在时,会自动创建 Draft Release。 + +发布流程会校验远端 Tag 指向的提交必须等于本次 workflow 的目标提交;如果 Tag 已指向其他提交,流程会在上传或更新 Release 前失败,避免旧 Tag 被错误产物覆盖。 + ## 许可证 [MIT](LICENSE) © 2026 CloverIris diff --git a/pubspec.yaml b/pubspec.yaml index a10ca7b..4ec68c5 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -30,10 +30,5 @@ dev_dependencies: drift_dev: ^2.32.1 build_runner: ^2.13.1 -hooks: - user_defines: - sqlite3: - source: system - flutter: uses-material-design: true diff --git a/scripts/setup.ps1 b/scripts/setup.ps1 new file mode 100644 index 0000000..b09191d --- /dev/null +++ b/scripts/setup.ps1 @@ -0,0 +1,41 @@ +# Papyrus 开发环境初始化脚本 (Windows) +$ErrorActionPreference = "Stop" + +$FlutterRoot = if ($env:FLUTTER_ROOT) { $env:FLUTTER_ROOT } else { "C:\src\flutter" } +$FlutterBin = Join-Path $FlutterRoot "bin" + +if (-not (Test-Path (Join-Path $FlutterBin "flutter.bat"))) { + Write-Host "正在安装 Flutter SDK 到 $FlutterRoot ..." + New-Item -ItemType Directory -Force -Path (Split-Path $FlutterRoot) | Out-Null + git clone https://github.com/flutter/flutter.git -b stable --depth 1 $FlutterRoot +} + +$userPath = [Environment]::GetEnvironmentVariable("Path", "User") +if ($userPath -notlike "*$FlutterBin*") { + [Environment]::SetEnvironmentVariable("Path", "$FlutterBin;$userPath", "User") + Write-Host "已将 Flutter 加入用户 PATH: $FlutterBin" +} + +$env:PATH = "$FlutterBin;$env:PATH" + +Push-Location (Split-Path $PSScriptRoot -Parent) +try { + Write-Host "检查 Flutter 环境..." + flutter doctor + + Write-Host "安装项目依赖..." + flutter pub get + + Write-Host "生成 Drift 代码(保持非破坏性)..." + dart run build_runner build + + Write-Host "运行单元测试..." + flutter test + + Write-Host "" + Write-Host "开发环境配置完成。" + Write-Host " Web: flutter run -d chrome" + Write-Host " Windows: 需安装 Visual Studio(Desktop development with C++)后执行 flutter run -d windows" +} finally { + Pop-Location +}