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
57 changes: 57 additions & 0 deletions .github/workflows/update-rules-bundle.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
name: Update bundled rules
on:
schedule:
- cron: '23 4 * * *'
workflow_dispatch:
inputs:
data_version:
description: Published immutable rules data version
required: false
type: string
permissions:
contents: write
pull-requests: write
concurrency:
group: update-bundled-rules
cancel-in-progress: false
jobs:
update:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-python@v6
with:
python-version: '3.12'
- name: Verify and pin immutable release
id: update
env:
DATA_VERSION: ${{ inputs.data_version }}
run: |
RULES_ROOT=https://raw.githubusercontent.com/LibChecker/LibChecker-Rules/rules-data
MANIFEST="$RULES_ROOT/manifest.json"
if [[ -n "$DATA_VERSION" ]]; then
[[ "$DATA_VERSION" =~ ^[1-9][0-9]*$ ]]
MANIFEST="$RULES_ROOT/releases/$DATA_VERSION/manifest.json"
fi
python3 tools/update-rules-bundle.py --manifest "$MANIFEST" --root "$RULES_ROOT"
if git diff --quiet -- library/src/main/assets/lcrules/v5 rules/manifest.json; then echo "changed=false" >> "$GITHUB_OUTPUT"; else echo "changed=true" >> "$GITHUB_OUTPUT"; fi
- uses: actions/setup-java@v6
with:
distribution: zulu
java-version: 17
- uses: android-actions/setup-android@v3
- uses: gradle/actions/setup-gradle@v6
- name: Verify updated data and package
if: steps.update.outputs.changed == 'true'
run: |
python3 -B -m unittest discover -s tools -p 'test_*.py'
./gradlew :library:assembleRelease :library:assembleDebugAndroidTest
- uses: peter-evans/create-pull-request@v7
with:
branch: automation/bundled-rules
commit-message: Update locked bundled rules
title: Update locked bundled rules
body: Verified immutable producer artifact and metadata. Review the data version and run Android validation before merging.
add-paths: |
library/src/main/assets/lcrules/v5/
rules/manifest.json
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ out/

# Gradle files
.gradle/
.kotlin/
**/build/

# Local configuration file (sdk path, etc)
Expand Down Expand Up @@ -84,4 +85,4 @@ library/debug/*
.project
.settings/org.eclipse.buildship.core.prefs
library/.settings/org.eclipse.buildship.core.prefs
.DS_Store
.DS_Store
188 changes: 169 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,21 +17,11 @@ dependencies {

## Quick Tutorial

Initialize SDK in `Application` class
Initialize the v5 baseline directly from the AAR:
```kotlin
class App : Application() {

override fun onCreate() {
super.onCreate()
LCRules.init(this)

// Optional: set online repo (GitHub repo as default)
LCRules.setRemoteRepo(LCRemoteRepo.GitHub)

// Optional: reserved for future rule locales
LCRules.setLocale(LCLocale.ZH)
}
}
LCRules.init(context)
// Select the existing cloud detail repository; icons come from Bundle drawables.
LCRules.setRemoteRepo(LCRemoteRepo.GitHub)
```

Get marked rule in a suspend context
Expand All @@ -51,20 +41,26 @@ val regexRule: Rule? = LCRules.getRule(
)
```

The SDK reads the bundled rules database through Android's SQLite APIs. It does
The SDK reads v5 data through Android's SQLite APIs. The AAR carries exactly one
baseline database and its metadata, with no v4 assets, descriptions or SVGs. It does
not require Room, AppCompat, Core KTX, or kotlinx-coroutines in host apps.
Choose the coroutine context for `getRule(...)` in your app.

## Migration

- Remove Room, AppCompat, and Core KTX dependencies if they were added only for
this package.
- Keep calling `LCRules.init(this)` from `Application.onCreate()`.
- Call `LCRules.init(context)` to validate, repair and select the AAR baseline.
It works independently, without an App database path or a second bundled copy.
- The app owns downloaded current/previous versions. After validating a candidate
newer than the baseline, call `LCRules.activateDatabase(file)` to switch. A failed
activation preserves the open baseline. There is no v4 fallback.
- `RuleReader.open` accepts only schema 5 and requires its sibling metadata.json.
The removed `getRulesAssetPath()` API no longer refers to any shipped asset.
- Use `LCRemoteRepo.GitHub` / `LCRemoteRepo.GitLab`. The old `Github` /
`Gitlab` aliases still work, but are deprecated.
- `LCRules.setLocale(...)` is kept for future rule locales. It is currently a
no-op because bundled rules contain one label locale.
- Use `LCRules.close()` instead of `closeDb()`.
- Use `LCRules.close()` to release the active reader. Removed unused APIs include
`closeDb`, `getVersion`, `getItemCounts`, `setLocale`, `readDetail` and `resolveAsset`.
- Do not use the old internal database classes (`RuleDao`, `RuleDatabase`,
`RuleRepository`, `Repositories`, `RuleEntity`, or `IAPI`). Query rules only
through `LCRules.getRule(...)`.
Expand All @@ -73,3 +69,157 @@ Choose the coroutine context for `getRule(...)` in your app.

JitPack builds with JDK 17 and publishes the library module through
`:library:publishToMavenLocal`.

## Bundled baseline and downloaded v5 data

`assets/lcrules/v5/rules.db` and `metadata.json` are the sole baseline data in the
AAR. `LCRules.init(context)` owns one persistent copy under
`noBackupFilesDir/lcrules-v5/bundled`. It compares both files with the packaged
assets, validates a staged copy before replacing a damaged cache, and recovers an
interrupted directory rename. Every call reselects the baseline, including when a
downloaded reader was previously active. Successful initialization removes temporary
staging/backup copies. The App must not install another copy of the baseline.

```kotlin
LCRules.init(context)
val baseline = LCRules.getMetadata() // dataVersion is Long
val database = LCRules.getDatabaseFile() // active, read-only path
// App Store checks archive hash, metadata, and candidate version > baseline.
RuleReader.open(downloadedDatabase).use { candidate ->
check(candidate.metadata.dataVersion > baseline.dataVersion)
}
LCRules.activateDatabase(downloadedDatabase)
```

The app's existing download Store retains and validates current/previous versions;
it falls back to the already-open AAR baseline if none is newer and valid. Same
versions prefer the AAR baseline. Activation is distinct from Context initialization;
`init(File)` is removed. `getDatabaseFile()` always identifies the active reader.
The corrected, unpublished dataVersion 45 Android ZIP contains exactly `rules.db`
and `metadata.json`, with no library descriptions, SVGs, icon index or fixtures.

`RuleReader.open` opens read-only, checks SQLite `quick_check`, required schema
and row values, compiles every regex, rejects duplicate type/name pairs, checks
metadata identity/count/reader compatibility. Invalid candidates throw without changing the
current singleton. It never downloads, repairs or deletes an app-selected file.
The app validates archive sizes/hashes before opening it and keeps installed
directories immutable while selected.

The Android table contains `_id`, `name`, `label`, `type`, `iconIndex`,
`isRegexRule`, `regexName`, `priority`, and nullable `labelEn`. Earlier eight-column
v5 databases remain readable and use `label` for every language. Other expanded v5 previews are
rejected; canonical and portable data retain their full source metadata.
Pass the persisted detail-language preference on each lookup:
`LCRules.getRule(name, type, useRegex, language = preferredRuleLanguage)`.
Chinese language tags (`"zh"`, `"zh-Hans"`, `"zh-Hant"`, case-insensitive, with `-` or `_` separators) select
the original Chinese `label`; `"en"`, other values and omitted
arguments select `labelEn`, falling back to `label` when English is absent.
The reader does not inspect system language or cache the selected language;
hosts must refresh any cached Rule objects after a preference change.
`RuleReader.getRule` exposes the same named `language` argument after `remoteRepo`.
`Rule` contains its original seven fields. `iconRes` and `isSimpleColorIcon` come
from Bundle's `IconResMap` and drawables; unknown or web-only indexes use the
placeholder. `descriptionUrl` uses the type/name or regexName cloud path and the
selected GitHub/GitLab root. SDK UUID comes from that cloud detail JSON.
XML icons and the sole v5 baseline remain in the AAR; v4 and version.prop are absent.

Exact lookup includes literal regex strings. v5 regex matching uses
whole-string matches in `(priority, _id)` order. Android ICU digit escapes are
normalized to ASCII, preserving escaped backslashes and character classes; v5
regex evaluation rejects the five line separators defined by the producer. Closing a reader and querying it
are synchronized. The singleton swaps only after a successful open and serializes
queries with initialization/close/activation; hosts must synchronize their own
download directory/pointer updates. Prune the download Store before activation,
and never prune Bundle's separate baseline directory.

`Rule` is a seven-field Parcelable data class with generated copy, destructuring,
equality and hash operations. Parcel bytes are not a persistent storage format.
Use `getMetadata().dataVersion` for the Long data version. Metadata access requires
an open v5 reader; it does not invent a fallback version before initialization or
after close.

## Unpublished local integration and checks

Build `:library:assembleRelease` in this checkout. The actual unpublished output
is `library/build/outputs/aar/library-release.aar`. The LibChecker app accepts:

```sh
./gradlew :app:assembleFossDebug \
-PrulesBundleAar=/absolute/path/to/LibChecker-Rules-Bundle/library/build/outputs/aar/library-release.aar
```

Its dependency selection uses `implementation(files(path))` instead of the normal
Maven dependency when that property is supplied. The library adds no runtime
dependencies beyond Kotlin already used by the app. Rebuild this AAR after Bundle
source changes. No publication or invented Maven version is required.

A composite `includeBuild` substitution can work when the host uses a compatible
Gradle/AGP toolchain. It does **not** currently work with LibChecker's Gradle 9.7 /
AGP 9.4: Bundle's AGP 8.11 references Gradle internals removed in Gradle 9.6.
Use the separately built AAR for this host. Keep this repository's Gradle wrapper
for its standalone checks. Library code releases and Rules data releases are
independent.

Build the library and test APK (JDK 17 for this standalone Gradle wrapper):

```sh
./gradlew :library:assembleRelease :library:assembleDebugAndroidTest \
-PrulesTestBundleDir=/absolute/path/to/unpacked/android-v5
```

The optional directory must be actual DB-only producer output. Tests verify
Context initialization/repair, update activation, all v5 record names, v4 rejection
without replacing current v5, cloud URL selection, drawable fallback and absence
of local details/icons. Without it, the producer integration test is explicitly
skipped. Test data is only included in the test APK.

Use a coordinated device window and install the test APK with `adb -s <serial>
install -r`; run selected checks with `adb -s <serial> shell am instrument -w -e
class <class#method> com.absinthe.lc.rulesbundle.test/androidx.test.runner.AndroidJUnitRunner`.
Do not use Gradle connected tasks on the shared device: UTP cleanup can uninstall
an app under test. Never clear or uninstall the user's app as part of validation.

## Sync Android drawable resources

From a committed Rules source revision:

```sh
python3 tools/sync_android_icons.py --rules-dir /path/to/LibChecker-Rules \
--revision <full-40-character-commit-sha>
python3 tools/sync_android_icons.py --rules-dir /path/to/LibChecker-Rules \
--revision <full-40-character-commit-sha> --check
python3 -B -m unittest discover -s tools -p 'test_*.py'
```

The script reads pinned `icons/index.json` and `icons/android/<iconId>.xml`
through Git, validates/copies actual VectorDrawables and generates `IconResMap`.
It rejects removal or reassignment of old indexes and missing previously shipped
vectors. New web-only indexes map to the placeholder until an Android XML exists.
The generated map records its source commit; Gradle compiles the copied XML.
No SVG-to-Android conversion or network fetch is involved.

Rules reusing shipped icons need only a data release. A new Android icon needs
this resource sync, a Bundle code/resource release and an App dependency update.
A data download cannot add a drawable to an already installed App. The original
XML-to-SVG producer converter continues to serve portable/web consumers.

## Update the packaged baseline

The small updater and automatic pull-request workflow now live in this repository.
Normal builds consume checked-in data; they do not fetch a floating release.

```sh
python3 tools/update-rules-bundle.py --manifest <producer-manifest> --root <producer-output-root>
python3 tools/update-rules-bundle.py --manifest <producer-manifest> --root <producer-output-root> --check
./gradlew :library:assembleRelease :library:assembleDebugAndroidTest
```

The updater verifies the Android ZIP SHA256, size, exact two-entry layout, extraction
limits and metadata identity. It writes only raw DB + metadata to AAR assets and
keeps the full producer lock at `rules/manifest.json`, outside packaged assets.
It never also stores the ZIP. An existing same-version lock can only be reverified,
not changed; changed data requires a forward version. The scheduled/manual
`.github/workflows/update-rules-bundle.yml` uses the same updater, builds the Bundle
and opens an own-repository PR. It does not publish a release or modify the App.
New Android icons still use the separate pinned resource sync and
require a resource release; a baseline data update does not create drawables.
8 changes: 8 additions & 0 deletions library/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ android {
targetSdk = 34
versionCode = 1
versionName = "1.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}

buildTypes {
Expand All @@ -40,8 +41,15 @@ android {
kotlinOptions {
jvmTarget = '11'
}

// Test real producer output without embedding a second copy in the library.
if (project.hasProperty('rulesTestBundleDir')) {
sourceSets.androidTest.assets.srcDir(project.property('rulesTestBundleDir'))
}
}

dependencies {
compileOnly 'androidx.annotation:annotation:1.9.1'
androidTestImplementation 'androidx.test:runner:1.7.0'
androidTestImplementation 'androidx.test.ext:junit:1.3.0'
}
Loading