Repository navigation
Apeiron 0.1.0: independent Minecraft launcher (Java Edition, C11 log core, bilingual UI) - #1
Conversation
The launcher must never block the Qt event loop on disk I/O and must never write credentials to disk. Both requirements live in C rather than Python: * bounded ring queue (4096 records) with a dedicated writer thread, so a caller never waits on the filesystem; * size-based rotation with retention, console + file sinks, ISO timestamps with milliseconds, PID/TID and exception context; * UTF-8 validation with explicit rejection of malformed input instead of writing mojibake; * redaction of keyword values (access_token, refresh_token, password, authorization, ...), Bearer credentials and JWT-shaped runs, applied before a record is queued; * portable threading layer (pthreads / Win32) so the same source builds with gcc, clang and MSVC; * standalone test harness (44 assertions) that runs without Python, plus the CPython bindings kept in a separate translation unit. The extension is compiled by setup.py and shipped inside the Nuitka artifact; it is not a separate service or shared library release.
Foundation shared by every feature: * AppPaths keeps every artefact under one per-user root, validates instance ids against traversal and creates directories with 0700 on POSIX. * Settings are validated and clamped on load, written atomically (fsync + os.replace), migrated from the v1 schema, and unknown keys from a newer build are preserved instead of dropped. * HttpClient is HTTPS-only (plain HTTP is allowed solely for loopback in tests), requires TLS 1.2+, refuses redirect downgrades, retries idempotent failures with jittered backoff and honours Retry-After. * SQLite uses WAL, foreign keys and append-only migrations recorded in PRAGMA user_version; repositories hold SQL only so business rules stay testable without a database. * DownloadManager provides bounded concurrency, per-attempt timeouts, cancellation within one chunk, .part files with fsync + atomic replace, SHA-1 verification, disk-space and writability pre-flight checks, duplicate suppression and structured progress dataclasses instead of log scraping.
…nces Java Edition * Version metadata is parsed faithfully: OS/arch/feature rule evaluation with the vanilla deny-by-default semantics, inheritsFrom merging, structured and legacy argument formats, and Maven-base library entries that mod loaders publish instead of resolved artefacts. * JavaInstaller is idempotent and content-addressed (libraries, natives with extract filters, client jar, asset index and objects, log4j config), so an interrupted installation resumes by rerunning it. * Java runtimes are never assumed: every candidate is executed with -version and parsed; a suitable Temurin build can be installed from the official Adoptium API with SHA-256 verification. * build_command() is a pure function producing per-version arguments, exactly one -cp pair, validated memory flags and a token-redacted launch log. * GameProcess drains both pipes on a thread, mirrors output to a per-launch log and reports a structured exit status. Accounts * Microsoft sign-in uses the official OAuth 2.0 device-code flow through Xbox Live and Minecraft services, performs an explicit ownership check and stores tokens only in the OS credential store (DPAPI / Keychain / libsecret), with a 0600 file fallback that the UI marks as insecure. No client id is shipped. * Offline profiles use the vanilla offline-UUID algorithm (verified against OpenJDK) and are always presented as unverified local identities. Content * Fabric and Quilt install from official metadata profiles. * Forge and NeoForge run the vendor installer only after its published SHA-1 matches, and only with explicit consent. * Modrinth .mrpack import/export; CurseForge import gated on the user's own API key; mods, resource packs, shaders and data packs with enable/disable. Instances and Bedrock * Create/copy/rename/delete with path guards, world backup/restore and archive export/import that omits credentials and caches. * Windows Bedrock adapter detects Store packages and activates them through shell:appsFolder; explicitly limited, no file modification or licence bypass.
* Eight pages (home, library, accounts, downloads, content, settings, logs, about) built on Qt Quick Controls' Material style. Qt implements Material 2, so the Material 3 semantic roles used by the design are provided explicitly by Apeiron/Components/Theme.qml instead of referencing properties that do not exist; the gap is documented rather than papered over. * Components live in one URI module (Apeiron.Components): importing the same directory through several relative paths produced duplicate type registries and made composite types fail to instantiate. * The backend is a context property with C++ ownership, a C++ parent and a strong reference: a Python-owned QObject exposed to QML was being collected, which surfaced as null-property errors in every binding. * Long work runs on a thread pool and reports through structured signals; the pool is drained before the engine is destroyed. * Language changes rebuild the component tree (cached qsTr bindings are not reliably re-evaluated by retranslate); UI state lives in the backend so the selection survives. * 179 strings translated to Simplified Chinese via Qt Linguist .ts/.qm, with a build script that fails when a qsTr string has no translation. * Keyboard shortcuts (Ctrl+1..8, Ctrl+L, Ctrl+Q), accessibility names and Chinese-capable font fallbacks. tools/gui_smoke.py exercises the real interface headlessly and fails on any Qt warning; tools/e2e_minecraft.py installs and launches the actual game.
169 pytest cases plus a 44-assertion standalone C harness cover the behaviour that is easy to get subtly wrong: * native logging: state errors, filtering, UTF-8 round-trip, rotation, concurrent writers, redaction before disk, shutdown idempotency; * downloader: success, duplicate skip, checksum mismatch, retry, 404, timeout, cancellation, disk space, unwritable target, concurrency, atomic replace; * version metadata: deny-by-default rules, inheritance, argument expansion, Maven-base libraries, classpath de-duplication; * launch: modern and legacy command construction, exactly one classpath pair, token redaction, process I/O and termination; * accounts: offline UUID equals the OpenJDK value, rename semantics, no secrets in SQLite, client-id requirement, device-code payload redaction; * security: zip/tar traversal, absolute paths, symlinks, size bombs, URL policy, update checksums, log redaction; * instances and modpacks: isolation, delete guards, backup/restore, export and import round-trip, unsafe pack paths, CurseForge key gating; * i18n and Bedrock: locale fallback chain, manual override, .qm loading, Appx parsing and unsupported-platform errors. GUI coverage runs the real QML interface in its own process (Qt teardown inside a shared pytest process is not deterministic) and asserts window creation, all eight pages, language switching in both directions, account and instance lifecycle, theme persistence, navigation and zero Qt warnings.
* README states the product position, supported platforms and versions, install and build steps, account modes, offline limitations, disclaimer and how to report a bug. * DISCLAIMER.md is the authoritative English statement; DISCLAIMER.zh-CN.md is marked in its header as a reference translation without independent legal effect, and docs/DISCLAIMER_POLICY.md records how the two relate. * docs/ documents architecture (including the Qt lifetime rules that cost real debugging time), the verified support matrix, account flows, building with Nuitka, the test layers, third-party licences and the i18n workflow. * CI checks formatting, lint, types, the C harness, the Python suite, i18n completeness, a headless QML load, and workflow syntax, with read-only permissions. The build workflow produces Nuitka artifacts for the three platforms and fails unless the packaged binary passes --self-check.
The negation re-included the compiled extension in the index; build artifacts must stay out of the repository.
The Adoptium CDN is throttled on some networks; refusing to fall back to a local runtime made the whole end-to-end check unverifiable there. The managed download path remains the default and is still exercised when no --java-path is supplied.
Mojang's downloads.client entry is a url/sha1/size triple with no Maven path. Reusing Artifact.from_dict (which requires a path) silently produced None, so every install failed at the client stage with "version ... declares no client download". The same applied to logging.client.file. Both are now built explicitly, defaulting the path to <id>.jar and the config id respectively, with regression tests covering the real payload shape and the absent case.
docs/TEST_REPORT.md lists the environment, every command that was run, the observed output, the defects the runs exposed (including the client-jar parsing bug that blocked every installation), and the capabilities that could not be verified here and why. No result in it is projected.
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
The first CI run exposed three real problems, all fixed here: * the lint job also covers packaging/, which had never been linted locally and failed on an unused noqa plus an outdated version guard; * the translation completeness check parsed qsTr line-by-line, so a line with two calls (e.g. Stop/Launch) produced garbage and false "missing" strings. It now scans each file with a proper regular expression and reports the number of occurrences it verified; * pip installs used trailing backslashes for continuation, which is a parse error under PowerShell, the default shell on windows runners. Workflow YAML still validates (7 jobs in ci.yml, 1 in build.yml).
The first green-on-Linux run was hiding assumptions that only held here: * the update-check tests hard-coded a "linux" asset entry, so on macOS and Windows the checker correctly found no asset and returned None. They now use current_platform_key(), which is what the code actually looks up. * the launch test compared argv[0] against "/usr/bin/java"; on Windows the same Path renders with backslashes. It now compares against str(Path(...)). * the GUI smoke asserted Chinese UI text while the product default is "follow system" and CI runners are English. The run now pins a Chinese locale, which is also the scenario the brief requires be tested (Chinese first start). * Qt emits a "Detected locale C ... not UTF-8" warning on hosts without an installed Chinese locale. That is a property of the machine, so it is excluded from the no-warnings gate; any QML warning still fails the run. The Windows GUI smoke crashes the offscreen scene graph (0xC0000005) and is skipped there with an explicit reason rather than being reported as passing; docs/TEST_REPORT.md records Windows GUI verification as not performed.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 43f7a16839
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if allow_error_body and "HTTP 400" in exc.message: | ||
| return {"error": "invalid_request", "error_description": exc.detail or ""} |
There was a problem hiding this comment.
Parse the OAuth error body while polling
During a normal device-code login, Microsoft's token endpoint returns protocol states such as authorization_pending and slow_down as HTTP 400 JSON responses. HttpClient discards that response body, and this handler converts every such response into the fabricated invalid_request value, so wait_for_token() fails on its first poll instead of waiting for the user to authorize. Preserve and parse the JSON error body for this endpoint.
Useful? React with 👍 / 👎.
| def native_artifact(self, platform_key: str) -> Artifact | None: | ||
| classifier = self.natives.get(platform_key) | ||
| if not classifier: | ||
| return None |
There was a problem hiding this comment.
Map macOS to Mojang's native classifier key
On Darwin, callers pass current_platform_key() == "macos", but Mojang version metadata keys native classifiers as osx. Consequently this lookup returns None for every macOS native library, the installer skips the LWJGL native jars, and Minecraft cannot start on a platform the launcher claims to support. Translate the application platform name to Mojang's osx vocabulary for classifier and rule evaluation.
Useful? React with 👍 / 👎.
| if [ "$RUNNER_OS" = "Windows" ]; then | ||
| BIN="dist/__main__.dist/Apeiron.exe" | ||
| else | ||
| BIN="dist/__main__.dist/Apeiron" | ||
| fi |
There was a problem hiding this comment.
Use the distribution directory produced from entry.py
The checked build workflow compiles packaging/entry.py, and packaging/build_nuitka.py itself searches entry.dist before __main__.dist; however, this smoke step hardcodes only dist/__main__.dist. When Nuitka uses the input module name and creates entry.dist, the build script's self-check succeeds but every matrix job then reports a missing artifact, and the archive step later hardcodes the same wrong directory.
Useful? React with 👍 / 👎.
| if loader and loader != "vanilla" and loader_version: | ||
| provider = self.ctx.modpacks.loaders.get(loader) | ||
| if provider is not None: | ||
| result = provider.install(mc_version, loader_version) |
There was a problem hiding this comment.
Add the required consent flow before installing Forge
When a user selects Forge or NeoForge in the new-instance dialog, this calls _InstallerJarLoader.install() without allow_execution=True; that method unconditionally rejects the call because no consent was supplied, and there is no subsequent confirmation path in the UI. The instance row and directory have already been created at this point, so the operation both fails every time and leaves an incomplete instance behind. Obtain explicit consent before creation or roll back and invoke the installer with that consent.
Useful? React with 👍 / 👎.
| fileMode: FileDialog.SaveFile | ||
| defaultSuffix: "apeiron.zip" | ||
| nameFilters: [qsTr("Apeiron instance (*.zip)"), qsTr("All files (*)")] | ||
| onAccepted: App.exportInstance(App.selectedInstanceId, selectedFile) |
There was a problem hiding this comment.
Convert FileDialog URLs to local filesystem paths
Qt Quick FileDialog.selectedFile is a URL, so a normal selection is passed as a value such as file:///home/user/export.zip. The Python slots accept it as a string and feed it directly to Path, which treats that value as a relative filesystem path rather than /home/user/export.zip; export therefore writes to an unintended file: tree, while import and content installation report that the selected file is missing. Pass selectedFile.toLocalFile() or accept a QUrl and call toLocalFile() in the backend.
Useful? React with 👍 / 👎.
| def deleteInstance(self, instance_id: str) -> None: | ||
| def work() -> dict[str, Any]: | ||
| instance = self.ctx.instances.get(instance_id) | ||
| self.ctx.instances.delete(instance_id) |
There was a problem hiding this comment.
Refuse to delete an instance while its game is running
The Library keeps its Delete action available during a launch, and this backend path removes the selected instance without checking launcher.process or launcher.state.instance_id. On POSIX, deleting the active instance succeeds while Java still has it as its working directory, removing worlds and configuration underneath the running game and causing later saves to fail; on platforms with locked files it instead leaves a partially deleted instance. Disable the action or reject deletion when this instance is active.
Useful? React with 👍 / 👎.
| def shutdown(self, timeout_ms: int = 5000) -> None: | ||
| """Stop background work before the engine is destroyed.""" | ||
| try: | ||
| self.ctx.launcher.stop() | ||
| except Exception: |
There was a problem hiding this comment.
Respect the launcher-exit process setting
Settings.close_game_on_launcher_exit defaults to False, but every shutdown unconditionally calls launcher.stop() here, and the application shutdown hook calls it again. Consequently closing Apeiron always terminates an active Minecraft process even when the persisted setting says to leave it running. Gate the stop call on that setting so the default behavior does not kill the user's game.
Useful? React with 👍 / 👎.
| except Exception as exc: | ||
| log.error("loader install failed for %s %s: %s", loader, version, exc) | ||
| return False |
There was a problem hiding this comment.
Propagate loader installation failures during modpack import
If the requested Fabric, Quilt, Forge, or NeoForge loader cannot be installed—for example because metadata is unavailable or Java is missing—this broad handler converts the failure to False. Both modpack import paths then continue installing files and return an ImportResult, so the UI reports a successful import even though the instance has no loaderVersionId and cannot launch. Let the loader error abort the import, ideally rolling back the newly created instance.
Useful? React with 👍 / 👎.
| for entry in index.files: | ||
| self._record_file( | ||
| instance, | ||
| instance_dir, | ||
| entry.path, | ||
| "mod", |
There was a problem hiding this comment.
Preserve each Modrinth file's content category
Every downloaded Modrinth entry is recorded as kind mod, even when its path is under resourcepacks/, shaderpacks/, or datapacks/. Such files consequently appear in the Mods tab, and toggle/removal operations look for their basename under the instance's mods directory, delete the database record, and leave the actual content untouched. Derive the record kind and managed relative location from entry.path instead of hardcoding mod.
Useful? React with 👍 / 👎.
| DownloadSpec( | ||
| url=entry.downloads[0], | ||
| destination=destination, | ||
| sha1=entry.sha1, | ||
| size=entry.size, | ||
| category="modpack", |
There was a problem hiding this comment.
Verify the SHA-512 declared by Modrinth packs
MrpackFile parses and retains sha512, but the generated DownloadSpec passes only sha1, and the downloader has no SHA-512 check. Because the parser also permits an entry with only a SHA-512 hash, such an untrusted pack causes the downloaded executable JAR to be committed with no checksum verification at all; even when both hashes exist, the stronger advertised digest is ignored. Require and verify the declared SHA-512 before installing the file.
Useful? React with 👍 / 👎.
* Every update-check test now builds its manifest for current_platform_key(); one of them still hard-coded a Linux asset entry, so macOS saw "no asset for this platform" and the checker correctly returned None. * The UI font list is filtered against installed families. Requesting absent families made Qt build alias tables on macOS and emit a warning that the no-warnings gate (correctly) rejected; the platform default is kept when no preferred family exists. * packaging/build_nuitka.py puts src/ on sys.path. CI runs it from a clean checkout without an installed distribution, where Nuitka failed with "failed to locate package 'apeiron'".
…I check * Nuitka runs as a subprocess, so inserting src/ into the parent's sys.path was invisible to it and CI still failed with "failed to locate package 'apeiron'". Child processes now receive PYTHONPATH including src/. * macOS offscreen rendering emits "Populating font family aliases ... missing font family Sans Serif", which is a property of the host's font set, not of the interface. It joins the locale warning as a documented exception to the no-warnings gate; QML warnings still fail the run.
--include-package is resolved through the interpreter's import system, so a subprocess started from a clean checkout could not see src/ even with PYTHONPATH exported, and every platform failed with "failed to locate package 'apeiron'". The build script now performs an idempotent editable install first.
… name Nuitka names the output directory after the compiled entry script, so the workflow looked for __main__.dist while the build produced entry.dist and every platform failed at the smoke step with a missing file.
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
The extension is included as a data file so that it lands in apeiron/ exactly as the frozen package expects. Nuitka's shared-library scanner also tried to copy it, which collided with the data file and crashed on macOS with NotADirectoryError; that scan is now excluded for _logcore.
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: a79148f3cc
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| uuid=uuid, | ||
| access_token=access_token, | ||
| user_type=user_type, | ||
| xuid=xuid, |
There was a problem hiding this comment.
Pass the OAuth client ID into launch options
For Minecraft versions whose game arguments contain --clientId ${clientid}, this constructs LaunchOptions without the client ID even though the authenticated session carries it. The substitution consequently produces an empty value, and build_command() removes that value while retaining --clientId, leaving the following option to be parsed as its argument and preventing affected modern versions from launching. Thread the resolved OAuth client ID through Backend.launchInstance() and LaunchService.launch() into these options.
Useful? React with 👍 / 👎.
| with zf.open(info) as src, target.open("wb") as dst: | ||
| dst.write(src.read()) |
There was a problem hiding this comment.
Limit override extraction before reading entries
When importing an untrusted modpack with a highly compressed override entry, src.read() materializes the entire decompressed file in memory and writes it without any aggregate or per-entry size limit. A crafted .mrpack can therefore exhaust memory or disk during a normal import; the CurseForge override extractor repeats the same pattern. Stream entries in bounded chunks while enforcing a decompressed-size budget, as the shared archive extractor already does.
Useful? React with 👍 / 👎.
| Component.onCompleted: { | ||
| for (var i = 0; i < App.accounts.length; ++i) { | ||
| if (App.accounts[i].id === App.selectedInstance.accountId) { | ||
| currentIndex = i | ||
| break | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
Resync the account selector when selection changes
This synchronization runs only when the component is first created, while the details panel remains instantiated as users select different instances. After switching from an instance using account A to one using account B, the combo box can still show A, and pressing Save passes A's ID to updateInstance(), silently rebinding the newly selected instance. Recompute currentIndex whenever App.selectedInstance.accountId or the account model changes.
Useful? React with 👍 / 👎.
| onAccepted: { | ||
| App.installContent(App.selectedInstanceId, selectedFile, page.kindFilter) | ||
| page.reload() |
There was a problem hiding this comment.
Refresh content after background mutations finish
installContent() only schedules a worker task, so this immediate reload normally queries the database before the copy and record insertion complete. The backend completion handler emits only a notification, and the page listens only for selection/instance changes, so newly installed content remains absent until an unrelated refresh; removal and toggling have the same race. Emit a content-changed signal or reload from the task's completion callback instead.
Useful? React with 👍 / 👎.
| def done(result: dict[str, Any]) -> None: | ||
| self.detect_java() | ||
| self.notify.emit("success", "java.installed", str(result.get("version", ""))) |
There was a problem hiding this comment.
Persist the managed Java runtime after installation
On a machine with no system Java—the primary scenario for these Install Java buttons—the successful runtime path is discarded and the callback merely rescans. detect_runtimes() searches system locations and configured extras, not ctx.paths.java, while the launcher is likewise wired to detect_runtimes() and an unchanged empty settings.java_path; the newly installed Temurin runtime therefore remains invisible and launching still reports that no Java exists. Save result["path"] as the configured runtime or include managed runtime directories in discovery.
Useful? React with 👍 / 👎.
| files = self.curseforge.install_files(manifest, instance_dir, self.downloader) | ||
| overrides = curseforge.extract_overrides(archive, manifest, instance_dir) |
There was a problem hiding this comment.
Record downloaded CurseForge files in the content repository
After a CurseForge import succeeds, this downloads all required mods but never creates corresponding ModRecord rows, unlike the Modrinth path. The Content page consequently reports no installed mods, toggle/removal cannot manage them, and exporting the instance as an mrpack omits every CurseForge-installed file. Preserve the resolved file metadata and add records after successful downloads.
Useful? React with 👍 / 👎.
| code = self.process.stop() | ||
| self.state = LaunchState(instance_id=self.state.instance_id, phase="exited", exit_code=code) |
There was a problem hiding this comment.
Preserve launch start time while stopping the game
When the user stops a running game, GameProcess.stop() and the pump thread's exit callback race after the child exits. If this assignment wins before _exit() computes its duration, it replaces self.state with a new state whose started_at is zero, so _exit() records a zero-second session and undercounts play time. Capture the launch timestamp in the callback closure or wait for the exit callback instead of overwriting the state here.
Useful? React with 👍 / 👎.
| command = build_command(installed, options) | ||
| log_path = create_launch_log(instance_dir) | ||
| record = self.launches.start(instance.id, str(log_path)) |
There was a problem hiding this comment.
Finalize the launch record when process startup fails
If GameProcess.start() raises—for example because the selected Java executable loses execute permission between probing and launch—the database row has already been inserted with status running, but the exit callback is never installed on a live process to finish it. The failed attempt then appears as running forever in recent launches. Create the record after successful process startup or mark it failed in the startup exception path.
Useful? React with 👍 / 👎.
| try: | ||
| return shlex.split(value, posix=True) | ||
| except ValueError: |
There was a problem hiding this comment.
Preserve Windows backslashes in custom launch arguments
On Windows, an unquoted custom argument such as -Dconfig=C:\mods\config.json is parsed with POSIX shell rules, which treat each backslash as an escape and produce -Dconfig=C:modsconfig.json. Since the settings UI exposes custom JVM arguments, ordinary Windows paths are silently corrupted before Java is started. Use platform-appropriate argument parsing or a parser that preserves Windows path separators.
Useful? React with 👍 / 👎.
| def _on_done(self, task_id: str, result: Any) -> None: | ||
| callback = self._pending.pop(task_id, None) | ||
| self._busy = "" | ||
| self.busyChanged.emit() |
There was a problem hiding this comment.
Clear completed download status when background work ends
Every downloader callback sets status.kind to download, including the final callback with all tasks complete, but successful and failed task handling only clears _busy and never transitions that status. After an install or import finishes, the app bar therefore continues showing a running spinner and the Downloads page keeps presenting the transfer as current until another operation happens to replace the status. Reset it to idle or a terminal state when the owning task completes or fails.
Useful? React with 👍 / 👎.
…failures CI is green on all 14 jobs across three operating systems and two Python versions, and Linux packaging succeeds inside CI. macOS and Windows packaging still fail at the Nuitka stage; the cause is not diagnosed and no artifact is claimed for them. The ten defects the CI loop exposed are listed individually.
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
修复 PR #1 审查问题、跨平台构建并接入优先镜像下载源
Apeiron 0.1.0 — full launcher implementation
Implements the launcher described in the task brief: a Chinese-first, bilingual
desktop launcher for Minecraft: Java Edition on Windows, macOS and Linux, with a
C11 logging core compiled into the application.
What is included
Native logging core (
csrc/,apeiron._logcore) — bounded queue, dedicatedwriter thread, size-based rotation with retention, console + file sinks,
millisecond timestamps, PID/TID, exception context, UTF-8 validation and
credential redaction. Portable across pthreads and Win32. Standalone C harness
(44 assertions) plus Python integration tests.
Java Edition pipeline — official manifest with cache; version inheritance;
deny-by-default rule evaluation; Maven-base library resolution; client,
libraries, natives and assets with SHA-1 verification; Java discovery by
execution (never by assumption) with optional Temurin provisioning; per-version
argument construction for both structured and legacy formats; process
supervision with pipe draining and log capture.
Accounts — Microsoft OAuth 2.0 device-code flow through Xbox Live and
Minecraft services with an explicit ownership check; tokens stored only in the
OS credential store (DPAPI / Keychain / libsecret) with a flagged file fallback;
offline profiles using the vanilla offline-UUID algorithm, always presented as
unverified.
Content — Fabric and Quilt from official metadata; Forge/NeoForge via the
vendor installer after SHA-1 verification and with explicit consent; Modrinth
.mrpackimport/export; CurseForge import gated on the user's own API key;mods, resource packs, shaders and data packs.
Instances — create, copy, rename, delete with path guards, world
backup/restore, archive export/import that omits credentials and caches.
UI — eight-page Material-style QML interface, light/dark/system themes,
keyboard shortcuts, accessibility names, 179 translated strings, Chinese default
with persistent manual override.
Security — safe archive extraction (traversal, absolute paths, symlinks,
size bombs), HTTPS-only transport, atomic verified downloads, log redaction,
instance-root guards.
Docs, licences, CI — README, authoritative English disclaimer plus a
Chinese reference translation, contributing/security/changelog, architecture,
platform, account, build, test, i18n, licence and disclaimer-policy documents;
CI for lint/types/C/Python/i18n/QML; Nuitka build workflow for three platforms.
Verification performed
pytest -qdocs/TEST_REPORT.mddocs/TEST_REPORT.mdruff check/ruff format --checkmypy src/apeirondocs/TEST_REPORT.mddocs/TEST_REPORT.md--self-checkdocs/TEST_REPORT.mddocs/TEST_REPORT.mdis the source of truth and records the environment, theexact commands, what passed, the failures found and fixed during development,
and — importantly — what could not be verified here.
Not verified here (stated, not hidden)
publisher and a licensed test account. The flow is implemented against the
official endpoints and fails with an actionable error when no client id is
configured.
environment; the Nuitka configuration is present and the Linux artifact was
built and self-checked.
Compatibility and risk
Essentials+Addons, the latter for theMaterial elevation effect).
signed yet; documented in
SECURITY.md.