Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e98bcf407c | |||
| f395d2515d | |||
| 3b01afeeb2 | |||
| b7e6a4b393 | |||
| 33659f6c05 | |||
| bd1e464b5a | |||
| 3d43420676 | |||
| c4576c84bb |
@@ -0,0 +1,7 @@
|
||||
AWS_ACCESS_KEY_ID=replace_with_access_key_id
|
||||
AWS_SECRET_ACCESS_KEY=replace_with_secret_access_key
|
||||
AWS_DEFAULT_REGION=ru-central1
|
||||
AWS_BUCKET=warcraft-client
|
||||
AWS_ENDPOINT=https://storage.yandexcloud.net
|
||||
AWS_USE_PATH_STYLE_ENDPOINT=true
|
||||
LAUNCHER_AUTH_KEY=replace_with_launcher_auth_key
|
||||
@@ -49,3 +49,8 @@ app.*.map.json
|
||||
|
||||
# Local launcher update signing material
|
||||
.moonwell_signing/
|
||||
|
||||
# Local production deployment credentials
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
|
||||
@@ -4,6 +4,14 @@ MoonWell Launcher is the desktop launcher for MoonWell, a private World of
|
||||
Warcraft: Wrath of the Lich King server focused on a player-friendly experience
|
||||
without excessive grind and with stronger social play.
|
||||
|
||||
The Windows executable requests administrator privileges on startup so it can
|
||||
synchronize game clients installed under protected directories such as
|
||||
`Program Files`.
|
||||
|
||||
Launcher version labels are read from the packaged application metadata. Change
|
||||
only the `version: X.Y.Z+N` value in `pubspec.yaml`; login, launcher status,
|
||||
Windows metadata, installer packaging, and AppCast are synchronized from it.
|
||||
|
||||
The launcher is responsible for:
|
||||
|
||||
- logging in to a player's MoonWell account
|
||||
@@ -108,6 +116,10 @@ Remove `-DryRun` only after reviewing the generated installer and AppCast.
|
||||
See `docs/production_deploy.md` for required environment variables, production
|
||||
defaults, safety checks, and failure recovery.
|
||||
|
||||
Local deploy credentials are read from the ignored root `.env`; use
|
||||
`.env.example` as the safe template. Process and CI environment variables
|
||||
override values from the file.
|
||||
|
||||
## Architecture
|
||||
|
||||
The UI lives in `lib/app`. Presentation-only design-system components live in
|
||||
|
||||
+8
-16
@@ -1,22 +1,14 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<rss version="2.0"
|
||||
xmlns:sparkle="http://www.andymatuschak.org/xml-namespaces/sparkle">
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<rss version="2.0" xmlns:sparkle="http://www.andymatuschak.org/xml-namespaces/sparkle">
|
||||
<channel>
|
||||
<title>MoonWell Launcher</title>
|
||||
<description>Обновления MoonWell Launcher</description>
|
||||
<description>MoonWell Launcher updates</description>
|
||||
<language>ru</language>
|
||||
<item>
|
||||
<title>MoonWell Launcher 1.0.1</title>
|
||||
<description>Обновление интерфейса, синхронизации клиента и интеграции с сервером MoonWell.</description>
|
||||
<pubDate>Tue, 28 Jul 2026 20:33:00 +0400</pubDate>
|
||||
<enclosure
|
||||
url="https://storage.yandexcloud.net/warcraft-client/moonwell_launcher_setup.exe"
|
||||
sparkle:version="1.0.1"
|
||||
sparkle:shortVersionString="1.0.1"
|
||||
sparkle:os="windows"
|
||||
sparkle:dsaSignature="MD4CHQCdoCW5jNymVlRblQpqy8C+jw4W0rVFQX0Bk25PAh0Av2ZcFYwZFQBQlAstQXna/PsuMNWIzXF4jpDuSA=="
|
||||
length="19989772"
|
||||
type="application/octet-stream" />
|
||||
<title>MoonWell Launcher 1.0.2</title>
|
||||
<description>Исправлено отображение версии и добавлен запуск с правами администратора.</description>
|
||||
<pubDate>Tue, 28 Jul 2026 21:23:41 +04:00</pubDate>
|
||||
<enclosure url="https://storage.yandexcloud.net/warcraft-client/moonwell_launcher_setup.exe" sparkle:version="1.0.2+4" sparkle:shortVersionString="1.0.2" sparkle:os="windows" sparkle:dsaSignature="MD0CHQDFH5GJKhykmXEZJTInFPrsoIkDc9tKroeR2XvJAhxrRjugtgetJlDiAhlyOtvgIYVRmAIb2sVqLelj" length="19994970" type="application/octet-stream" />
|
||||
</item>
|
||||
</channel>
|
||||
</rss>
|
||||
</rss>
|
||||
@@ -9,6 +9,11 @@ WinSparkle shows its native prompt only when a newer version is available.
|
||||
Missing configuration, network failures, and malformed feeds do not block the
|
||||
launcher. Periodic WinSparkle checks are disabled.
|
||||
|
||||
`MoonWell.exe` uses the Windows `requireAdministrator` execution level. Users
|
||||
therefore receive a UAC prompt whenever the launcher starts. This is required
|
||||
to synchronize game files installed in protected directories. The per-user
|
||||
Inno Setup installer remains configured with `PrivilegesRequired=lowest`.
|
||||
|
||||
## Build Configuration
|
||||
|
||||
Pass the public feed URL at build time:
|
||||
@@ -45,8 +50,11 @@ certificate would not establish trust on player machines.
|
||||
## Version and AppCast Contract
|
||||
|
||||
Increase the `X.Y.Z` version in `pubspec.yaml` for every launcher release. A
|
||||
build-number-only change such as `1.0.0+2` is not sufficient because WinSparkle
|
||||
compares the Windows product version. Pass the same `X.Y.Z` value to Inno Setup:
|
||||
build-number-only change such as `1.0.0+2` is not sufficient for a public
|
||||
release. Flutter writes the complete `X.Y.Z+N` value into the Windows
|
||||
`ProductVersion`; `sparkle:version` must match that complete value exactly.
|
||||
Use `sparkle:shortVersionString` for the human-readable `X.Y.Z` value. Pass
|
||||
`X.Y.Z` without the build suffix to Inno Setup:
|
||||
|
||||
```powershell
|
||||
ISCC.exe /DMyAppVersion=1.1.0 installer\moonwell_launcher.iss
|
||||
@@ -70,7 +78,8 @@ Use a public AppCast with absolute URLs:
|
||||
<pubDate>Sun, 21 Jun 2026 12:00:00 +0400</pubDate>
|
||||
<enclosure
|
||||
url="https://storage.yandexcloud.net/warcraft-client/moonwell_launcher_setup.exe"
|
||||
sparkle:version="1.1.0"
|
||||
sparkle:version="1.1.0+3"
|
||||
sparkle:shortVersionString="1.1.0"
|
||||
sparkle:os="windows"
|
||||
sparkle:dsaSignature="SIGNATURE_FROM_SIGN_UPDATE"
|
||||
length="ACTUAL_INSTALLER_SIZE_IN_BYTES"
|
||||
|
||||
@@ -37,14 +37,29 @@ Install the Python dependency if required:
|
||||
python -m pip install boto3
|
||||
```
|
||||
|
||||
The PowerShell entry point calls `tool/upload_launcher_release.py` as a regular
|
||||
Python file. Keep this helper beside the deployment script; it performs the S3
|
||||
upload and verifies the uploaded object's byte length.
|
||||
|
||||
Back up `.moonwell_signing\dsa_priv.pem` in the project secret store. Never
|
||||
commit it, upload it to Object Storage, or send it through chat. Losing this key
|
||||
prevents released launchers from accepting future updates.
|
||||
|
||||
## Required Environment
|
||||
## Local `.env`
|
||||
|
||||
The script reads secrets and storage configuration only from environment
|
||||
variables:
|
||||
For local releases, copy `.env.example` to `.env` and fill in the real values.
|
||||
The repository ignores `.env` and every `.env.*` variant except the safe
|
||||
`.env.example` template.
|
||||
|
||||
```powershell
|
||||
Copy-Item .env.example .env
|
||||
```
|
||||
|
||||
The deploy script loads `.env` automatically. Variables already present in the
|
||||
current process or CI environment take precedence over the file. Use
|
||||
`-EnvFile C:\secure\moonwell.env` to select a different local file.
|
||||
|
||||
The following values are supported:
|
||||
|
||||
| Variable | Required | Purpose |
|
||||
| --- | --- | --- |
|
||||
@@ -56,8 +71,8 @@ variables:
|
||||
| `AWS_BUCKET` | optional | Defaults to `warcraft-client` |
|
||||
| `AWS_USE_PATH_STYLE_ENDPOINT` | optional | Defaults to `true` |
|
||||
|
||||
Load secrets from the team secret manager into the current PowerShell process.
|
||||
Do not put real values in a tracked `.env` file or in the script.
|
||||
Production CI should load these values from the team secret manager instead of
|
||||
creating `.env`. Never commit the real file or put its contents into logs.
|
||||
|
||||
## Prepare a Release
|
||||
|
||||
@@ -67,9 +82,16 @@ Increase both parts of the version in `pubspec.yaml`:
|
||||
version: 1.0.2+3
|
||||
```
|
||||
|
||||
`X.Y.Z` is the WinSparkle release version. `N` is the Flutter build number.
|
||||
Every production release must have an `X.Y.Z` value greater than the version
|
||||
currently published in AppCast.
|
||||
`X.Y.Z` is the public launcher version. `N` is the Flutter build number.
|
||||
Flutter places the complete `X.Y.Z+N` value in the Windows `ProductVersion`,
|
||||
so the script writes that same complete value to `sparkle:version` and writes
|
||||
`X.Y.Z` to `sparkle:shortVersionString`. Every production release must have an
|
||||
`X.Y.Z` value greater than the release currently published in AppCast.
|
||||
|
||||
The UI reads `X.Y.Z` from the packaged application metadata through
|
||||
`package_info_plus`. Version labels on the login and launcher screens therefore
|
||||
update automatically after changing `pubspec.yaml`; do not add separate
|
||||
hardcoded version strings.
|
||||
|
||||
Run a dry run first:
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:moonwell_launcher/app/launcher_version.dart';
|
||||
import 'package:moonwell_launcher/app/design_system/mw_shells.dart';
|
||||
import 'package:moonwell_launcher/app/theme/moonwell_design_system.dart';
|
||||
|
||||
@@ -47,6 +48,7 @@ class _MwAuthenticationFormState extends State<MwAuthenticationForm> {
|
||||
Widget build(BuildContext context) {
|
||||
final accent = MoonWellDesignTokens.of(context).accent;
|
||||
final registering = widget.mode == MwAuthenticationMode.registration;
|
||||
final launcherVersion = LauncherVersionScope.of(context);
|
||||
return AutofillGroup(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
@@ -259,10 +261,10 @@ class _MwAuthenticationFormState extends State<MwAuthenticationForm> {
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 28),
|
||||
const Center(
|
||||
Center(
|
||||
child: Text(
|
||||
'MOONWELL · V 1.0.0 · PLAY.MOON-WELL.ONLINE',
|
||||
style: TextStyle(
|
||||
'MOONWELL · V $launcherVersion · PLAY.MOON-WELL.ONLINE',
|
||||
style: const TextStyle(
|
||||
fontFamily: 'JetBrains Mono',
|
||||
color: Color(0x52ECE4D0),
|
||||
fontSize: 8,
|
||||
@@ -311,9 +313,7 @@ class _Tab extends StatelessWidget {
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
border: Border(
|
||||
bottom: BorderSide(
|
||||
color: selected ? accent : const Color(0x247FB8D4),
|
||||
),
|
||||
bottom: BorderSide(color: selected ? accent : Colors.transparent),
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:moonwell_launcher/app/launcher_version.dart';
|
||||
import 'package:moonwell_launcher/app/design_system/mw_primitives.dart';
|
||||
import 'package:moonwell_launcher/app/theme/moonwell_design_system.dart';
|
||||
|
||||
@@ -718,37 +719,40 @@ class MwLauncherStatusBar extends StatelessWidget {
|
||||
final String? eta;
|
||||
final String? buildHash;
|
||||
@override
|
||||
Widget build(BuildContext context) => Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 32),
|
||||
decoration: const BoxDecoration(
|
||||
color: Color(0xD904060E),
|
||||
border: Border(top: BorderSide(color: _border)),
|
||||
),
|
||||
child: DefaultTextStyle(
|
||||
style: const TextStyle(
|
||||
fontFamily: 'JetBrains Mono',
|
||||
color: _muted,
|
||||
fontSize: 8,
|
||||
letterSpacing: .8,
|
||||
Widget build(BuildContext context) {
|
||||
final launcherVersion = LauncherVersionScope.of(context);
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 32),
|
||||
decoration: const BoxDecoration(
|
||||
color: Color(0xD904060E),
|
||||
border: Border(top: BorderSide(color: _border)),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(status, maxLines: 1, overflow: TextOverflow.ellipsis),
|
||||
),
|
||||
if (fileCount != null) Text(fileCount!),
|
||||
if (speed != null) ...[const SizedBox(width: 24), Text(speed!)],
|
||||
if (eta != null) ...[const SizedBox(width: 24), Text(eta!)],
|
||||
if (buildHash != null) ...[
|
||||
child: DefaultTextStyle(
|
||||
style: const TextStyle(
|
||||
fontFamily: 'JetBrains Mono',
|
||||
color: _muted,
|
||||
fontSize: 8,
|
||||
letterSpacing: .8,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(status, maxLines: 1, overflow: TextOverflow.ellipsis),
|
||||
),
|
||||
if (fileCount != null) Text(fileCount!),
|
||||
if (speed != null) ...[const SizedBox(width: 24), Text(speed!)],
|
||||
if (eta != null) ...[const SizedBox(width: 24), Text(eta!)],
|
||||
if (buildHash != null) ...[
|
||||
const SizedBox(width: 24),
|
||||
Text(buildHash!),
|
||||
],
|
||||
const SizedBox(width: 24),
|
||||
Text(buildHash!),
|
||||
Text('Лаунчер v$launcherVersion'),
|
||||
],
|
||||
const SizedBox(width: 24),
|
||||
const Text('Лаунчер v1.0.0'),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class MwAccountAffordance extends StatelessWidget {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:moonwell_launcher/app/launcher_version.dart';
|
||||
import 'package:moonwell_launcher/app/design_system/mw_launcher_components.dart';
|
||||
import 'package:moonwell_launcher/app/design_system/mw_window_chrome.dart';
|
||||
import 'package:moonwell_launcher/app/theme/moonwell_design_system.dart';
|
||||
@@ -302,21 +303,22 @@ class _LoginStatusBar extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final launcherVersion = LauncherVersionScope.of(context);
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 32),
|
||||
decoration: const BoxDecoration(
|
||||
color: Color(0xD904060E),
|
||||
border: Border(top: BorderSide(color: Color(0x247FB8D4))),
|
||||
),
|
||||
child: const Row(
|
||||
child: Row(
|
||||
children: [
|
||||
Text('Версия лаунчера 1.0.0'),
|
||||
SizedBox(width: 24),
|
||||
Text('·'),
|
||||
SizedBox(width: 24),
|
||||
Text('Соединение защищено TLS'),
|
||||
Spacer(),
|
||||
Text('© 2026 MoonWell'),
|
||||
Text('Версия лаунчера $launcherVersion'),
|
||||
const SizedBox(width: 24),
|
||||
const Text('·'),
|
||||
const SizedBox(width: 24),
|
||||
const Text('Соединение защищено TLS'),
|
||||
const Spacer(),
|
||||
const Text('© 2026 MoonWell'),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
class LauncherVersionScope extends InheritedWidget {
|
||||
const LauncherVersionScope({
|
||||
super.key,
|
||||
required this.version,
|
||||
required super.child,
|
||||
});
|
||||
|
||||
final String version;
|
||||
|
||||
static String of(BuildContext context) {
|
||||
return context
|
||||
.dependOnInheritedWidgetOfExactType<LauncherVersionScope>()
|
||||
?.version ??
|
||||
'dev';
|
||||
}
|
||||
|
||||
@override
|
||||
bool updateShouldNotify(LauncherVersionScope oldWidget) {
|
||||
return version != oldWidget.version;
|
||||
}
|
||||
}
|
||||
+14
-8
@@ -1,6 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:moonwell_launcher/app/design_system/mw_shells.dart';
|
||||
import 'package:moonwell_launcher/app/home_screen/home_screen.dart';
|
||||
import 'package:moonwell_launcher/app/launcher_version.dart';
|
||||
import 'package:moonwell_launcher/app/login_screen/login_screen.dart';
|
||||
import 'package:moonwell_launcher/app/theme/moonwell_design_system.dart';
|
||||
import 'package:moonwell_launcher/features/launcher/application/restore_launcher_session_use_case.dart';
|
||||
@@ -9,7 +10,9 @@ import 'package:moonwell_launcher/features/preferences/domain/repositories/prefe
|
||||
import 'package:moonwell_launcher/service_container.dart';
|
||||
|
||||
class MoonWellApp extends StatefulWidget {
|
||||
const MoonWellApp({super.key});
|
||||
const MoonWellApp({super.key, required this.launcherVersion});
|
||||
|
||||
final String launcherVersion;
|
||||
|
||||
@override
|
||||
State<MoonWellApp> createState() => _MoonWellAppState();
|
||||
@@ -35,13 +38,16 @@ class _MoonWellAppState extends State<MoonWellApp> {
|
||||
Widget build(BuildContext context) {
|
||||
return AnimatedBuilder(
|
||||
animation: _themeController,
|
||||
builder: (context, _) => LauncherThemeScope(
|
||||
controller: _themeController,
|
||||
child: MaterialApp(
|
||||
title: 'MoonWell',
|
||||
debugShowCheckedModeBanner: false,
|
||||
home: const _LauncherBootstrapScreen(),
|
||||
theme: MoonWellTheme.create(_themeController.variant),
|
||||
builder: (context, _) => LauncherVersionScope(
|
||||
version: widget.launcherVersion,
|
||||
child: LauncherThemeScope(
|
||||
controller: _themeController,
|
||||
child: MaterialApp(
|
||||
title: 'MoonWell',
|
||||
debugShowCheckedModeBanner: false,
|
||||
home: const _LauncherBootstrapScreen(),
|
||||
theme: MoonWellTheme.create(_themeController.variant),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -11,6 +11,7 @@ final class Config {
|
||||
static const launcherMetadataDirectoryName = '.moonwell_launcher';
|
||||
static const hashCacheFileName = 'hash_cache.json';
|
||||
static const ignoredVerificationDirectories = <String>{
|
||||
'addons',
|
||||
'cache',
|
||||
'errors',
|
||||
'logs',
|
||||
|
||||
@@ -2,6 +2,7 @@ import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:injectable/injectable.dart';
|
||||
import 'package:moonwell_launcher/config.dart';
|
||||
import 'package:moonwell_launcher/core/use_case.dart';
|
||||
import 'package:moonwell_launcher/features/downloader/domain/entities/download_exceptions.dart';
|
||||
import 'package:moonwell_launcher/features/downloader/domain/entities/download_progress.dart';
|
||||
@@ -95,6 +96,7 @@ class ClientSyncUseCase
|
||||
final localSnapshot = await _installationService.scanInstallation(
|
||||
input.request.installationDir,
|
||||
isCancelled: input.isCancelled,
|
||||
manifest: manifest,
|
||||
onProgress: (progress, currentPath, processedFiles, totalFiles) {
|
||||
_emit(
|
||||
controller,
|
||||
@@ -185,6 +187,7 @@ class ClientSyncUseCase
|
||||
final verifiedSnapshot = await _installationService.scanInstallation(
|
||||
input.request.installationDir,
|
||||
isCancelled: input.isCancelled,
|
||||
manifest: manifest,
|
||||
onProgress: (progress, currentPath, processedFiles, totalFiles) {
|
||||
_emit(
|
||||
controller,
|
||||
@@ -279,7 +282,13 @@ class ClientSyncUseCase
|
||||
final remoteFiles = manifest.filesByPath;
|
||||
|
||||
return snapshot.files
|
||||
.where((localFile) => !remoteFiles.containsKey(localFile.path))
|
||||
.where((localFile) {
|
||||
if (remoteFiles.containsKey(localFile.path)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return !_isIgnored(localFile.path);
|
||||
})
|
||||
.map((file) => file.path)
|
||||
.toList();
|
||||
}
|
||||
@@ -454,14 +463,21 @@ class ClientSyncUseCase
|
||||
|
||||
final mismatchedFiles = manifest.files.where((remoteFile) {
|
||||
final localFile = localFilesByPath[remoteFile.path];
|
||||
return localFile == null ||
|
||||
final isMismatched =
|
||||
localFile == null ||
|
||||
localFile.sha256 != remoteFile.sha256 ||
|
||||
localFile.size != remoteFile.size;
|
||||
|
||||
return isMismatched;
|
||||
}).toList();
|
||||
|
||||
final extraFiles = snapshot.files
|
||||
.where((localFile) => !manifest.filesByPath.containsKey(localFile.path))
|
||||
.toList();
|
||||
final extraFiles = snapshot.files.where((localFile) {
|
||||
if (manifest.filesByPath.containsKey(localFile.path)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return !_isIgnored(localFile.path);
|
||||
}).toList();
|
||||
|
||||
if (snapshot.buildHash != remoteBuildHash ||
|
||||
mismatchedFiles.isNotEmpty ||
|
||||
@@ -492,6 +508,15 @@ class ClientSyncUseCase
|
||||
}
|
||||
}
|
||||
|
||||
/// Файлы, которые не будут удалены во время проверки файлов. Например, пользовательские
|
||||
/// аддоны, скриншоты и т.д.
|
||||
bool _isIgnored(String filePath) {
|
||||
final normalizedPath = filePath.replaceAll('\\', '/').toLowerCase();
|
||||
return Config.ignoredVerificationDirectories.any(
|
||||
(pattern) => normalizedPath.contains(pattern.toLowerCase()),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _verifyDownloadedFile({
|
||||
required String installationDir,
|
||||
required ClientManifestFile file,
|
||||
|
||||
@@ -8,10 +8,11 @@ import 'package:injectable/injectable.dart';
|
||||
import 'package:moonwell_launcher/config.dart';
|
||||
import 'package:moonwell_launcher/features/downloader/domain/entities/download_exceptions.dart';
|
||||
import 'package:moonwell_launcher/features/downloader/domain/entities/download_progress.dart';
|
||||
import 'package:moonwell_launcher/features/launcher/domain/entities/client_hash_entry.dart';
|
||||
import 'package:moonwell_launcher/features/launcher/domain/entities/client_installation_snapshot.dart';
|
||||
import 'package:moonwell_launcher/features/launcher/domain/entities/client_manifest.dart';
|
||||
import 'package:moonwell_launcher/features/launcher/domain/entities/launcher_exception.dart';
|
||||
import 'package:moonwell_launcher/features/launcher/domain/entities/local_client_file.dart';
|
||||
import 'package:moonwell_launcher/features/launcher/domain/entities/client_hash_entry.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
|
||||
typedef InstallationScanProgressCallback =
|
||||
@@ -33,6 +34,7 @@ final class GameProcessHandle {
|
||||
class GameInstallationService {
|
||||
Future<ClientInstallationSnapshot> scanInstallation(
|
||||
String installationDir, {
|
||||
required ClientManifest manifest,
|
||||
InstallationScanProgressCallback? onProgress,
|
||||
FutureOr<bool> Function()? isCancelled,
|
||||
}) async {
|
||||
@@ -132,6 +134,7 @@ class GameInstallationService {
|
||||
'hashCachePath': hashCachePath,
|
||||
'cachedHashes': cachedHashes,
|
||||
'ignoredDirectories': Config.ignoredVerificationDirectories.toList(),
|
||||
'serverFiles': manifest.files.map((f) => f.path).toList(),
|
||||
},
|
||||
onError: errorPort.sendPort,
|
||||
onExit: exitPort.sendPort,
|
||||
@@ -343,6 +346,9 @@ Future<void> _scanInstallationIsolateMain(Map<String, Object?> message) async {
|
||||
.whereType<String>()
|
||||
.map((entry) => entry.toLowerCase())
|
||||
.toSet();
|
||||
final serverFiles = (message['serverFiles'] as List<Object?>)
|
||||
.whereType<String>()
|
||||
.toSet();
|
||||
|
||||
final rootDirectory = Directory(installationDir);
|
||||
if (!await rootDirectory.exists()) {
|
||||
@@ -356,6 +362,7 @@ Future<void> _scanInstallationIsolateMain(Map<String, Object?> message) async {
|
||||
rootDirectory.path,
|
||||
pendingFiles,
|
||||
ignoredDirectories,
|
||||
serverFiles,
|
||||
);
|
||||
|
||||
pendingFiles.sort(
|
||||
@@ -424,41 +431,21 @@ Future<void> _collectFilesInIsolate(
|
||||
String rootPath,
|
||||
List<_SerializablePendingFile> pendingFiles,
|
||||
Set<String> ignoredDirectories,
|
||||
Set<String> serverFiles,
|
||||
) async {
|
||||
await for (final entity in directory.list(followLinks: false)) {
|
||||
final relativePath = normalizeClientPath(
|
||||
p.relative(entity.path, from: rootPath),
|
||||
);
|
||||
for (final path in serverFiles) {
|
||||
final file = File('$rootPath/$path');
|
||||
|
||||
if (relativePath.isEmpty || relativePath == '.') {
|
||||
if (!await file.exists()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
final segments = p.posix.split(relativePath);
|
||||
if (segments.isNotEmpty &&
|
||||
ignoredDirectories.contains(segments.first.toLowerCase())) {
|
||||
continue;
|
||||
}
|
||||
final stat = await file.stat();
|
||||
|
||||
if (entity is Directory) {
|
||||
await _collectFilesInIsolate(
|
||||
entity,
|
||||
rootPath,
|
||||
pendingFiles,
|
||||
ignoredDirectories,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (entity is! File) {
|
||||
continue;
|
||||
}
|
||||
|
||||
final stat = await entity.stat();
|
||||
pendingFiles.add(
|
||||
_SerializablePendingFile(
|
||||
path: entity.path,
|
||||
relativePath: relativePath,
|
||||
path: file.path,
|
||||
relativePath: normalizeClientPath(p.relative(path)),
|
||||
size: stat.size,
|
||||
modifiedMs: stat.modified.millisecondsSinceEpoch,
|
||||
),
|
||||
|
||||
+12
-1
@@ -5,6 +5,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:moonwell_launcher/app/mw_app.dart';
|
||||
import 'package:moonwell_launcher/features/launcher/data/launcher_update_service.dart';
|
||||
import 'package:moonwell_launcher/service_container.dart';
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
import 'package:window_manager/window_manager.dart';
|
||||
|
||||
Future<void> main() async {
|
||||
@@ -13,6 +14,7 @@ Future<void> main() async {
|
||||
|
||||
await configureDependencies(env: 'flutter');
|
||||
final launcherUpdateService = getIt<LauncherUpdateService>();
|
||||
final launcherVersion = await _readLauncherVersion();
|
||||
|
||||
const windowOptions = WindowOptions(
|
||||
size: Size(1280, 720),
|
||||
@@ -31,5 +33,14 @@ Future<void> main() async {
|
||||
}),
|
||||
);
|
||||
|
||||
runApp(const MoonWellApp());
|
||||
runApp(MoonWellApp(launcherVersion: launcherVersion));
|
||||
}
|
||||
|
||||
Future<String> _readLauncherVersion() async {
|
||||
try {
|
||||
final packageInfo = await PackageInfo.fromPlatform();
|
||||
return packageInfo.version;
|
||||
} catch (_) {
|
||||
return 'dev';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
C:/Users/sindo/AppData/Local/Pub/Cache/hosted/pub.dev/package_info_plus-9.0.1/
|
||||
@@ -8,6 +8,7 @@ import Foundation
|
||||
import auto_updater_macos
|
||||
import file_picker
|
||||
import flutter_secure_storage_darwin
|
||||
import package_info_plus
|
||||
import screen_retriever_macos
|
||||
import shared_preferences_foundation
|
||||
import url_launcher_macos
|
||||
@@ -17,6 +18,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||
AutoUpdaterMacosPlugin.register(with: registry.registrar(forPlugin: "AutoUpdaterMacosPlugin"))
|
||||
FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin"))
|
||||
FlutterSecureStorageDarwinPlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStorageDarwinPlugin"))
|
||||
FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin"))
|
||||
ScreenRetrieverMacosPlugin.register(with: registry.registrar(forPlugin: "ScreenRetrieverMacosPlugin"))
|
||||
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
||||
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
|
||||
|
||||
@@ -424,6 +424,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.4.0"
|
||||
http:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: http
|
||||
sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.6.0"
|
||||
http_multi_server:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -592,6 +600,22 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.0"
|
||||
package_info_plus:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: package_info_plus
|
||||
sha256: "468c26b4254ab01979fa5e4a98cb343ea3631b9acee6f21028997419a80e1a20"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "9.0.1"
|
||||
package_info_plus_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: package_info_plus_platform_interface
|
||||
sha256: "202a487f08836a592a6bd4f901ac69b3a8f146af552bbd14407b6b41e1c3f086"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.2.1"
|
||||
path:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
||||
+2
-1
@@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev
|
||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||
# In Windows, build-name is used as the major, minor, and patch parts
|
||||
# of the product and file versions while build-number is used as the build suffix.
|
||||
version: 1.0.1+2
|
||||
version: 1.0.2+4
|
||||
|
||||
environment:
|
||||
sdk: ">=3.10.0"
|
||||
@@ -47,6 +47,7 @@ dependencies:
|
||||
window_manager: ^0.5.1
|
||||
auto_updater: 1.0.0
|
||||
url_launcher: ^6.3.2
|
||||
package_info_plus: ^9.0.1
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:moonwell_launcher/app/design_system/mw_authentication.dart';
|
||||
import 'package:moonwell_launcher/app/launcher_version.dart';
|
||||
import 'package:moonwell_launcher/app/theme/moonwell_design_system.dart';
|
||||
|
||||
void main() {
|
||||
@@ -81,4 +82,62 @@ void main() {
|
||||
|
||||
expect(requestedMode, MwAuthenticationMode.registration);
|
||||
});
|
||||
|
||||
testWidgets('inactive authentication tab has no divider', (tester) async {
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
theme: MoonWellTheme.create(MoonWellThemeVariant.forest),
|
||||
home: Scaffold(
|
||||
body: MwAuthenticationForm(
|
||||
usernameController: TextEditingController(),
|
||||
passwordController: TextEditingController(),
|
||||
onSubmit: () {},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
final tabBorders = tester
|
||||
.widgetList<Container>(
|
||||
find.descendant(
|
||||
of: find.byType(MwAuthenticationForm),
|
||||
matching: find.byType(Container),
|
||||
),
|
||||
)
|
||||
.map((container) => container.decoration)
|
||||
.whereType<BoxDecoration>()
|
||||
.map((decoration) => decoration.border)
|
||||
.whereType<Border>()
|
||||
.where((border) => border.bottom.style != BorderStyle.none)
|
||||
.map((border) => border.bottom.color)
|
||||
.toList();
|
||||
|
||||
expect(tabBorders, hasLength(2));
|
||||
expect(
|
||||
tabBorders.where((color) => color == Colors.transparent),
|
||||
hasLength(1),
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('authentication footer uses the packaged launcher version', (
|
||||
tester,
|
||||
) async {
|
||||
await tester.pumpWidget(
|
||||
LauncherVersionScope(
|
||||
version: '9.8.7',
|
||||
child: MaterialApp(
|
||||
theme: MoonWellTheme.create(MoonWellThemeVariant.forest),
|
||||
home: Scaffold(
|
||||
body: MwAuthenticationForm(
|
||||
usernameController: TextEditingController(),
|
||||
passwordController: TextEditingController(),
|
||||
onSubmit: () {},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(find.textContaining('V 9.8.7'), findsOneWidget);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:moonwell_launcher/app/design_system/mw_launcher_components.dart';
|
||||
import 'package:moonwell_launcher/app/launcher_version.dart';
|
||||
import 'package:moonwell_launcher/app/theme/moonwell_design_system.dart';
|
||||
|
||||
void main() {
|
||||
@@ -137,6 +138,25 @@ void main() {
|
||||
expect(find.text('Проверить файлы'), findsOneWidget);
|
||||
expect(find.text('Выйти'), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('status bar uses the packaged launcher version', (tester) async {
|
||||
await tester.pumpWidget(
|
||||
LauncherVersionScope(
|
||||
version: '9.8.7',
|
||||
child: MaterialApp(
|
||||
theme: MoonWellTheme.create(MoonWellThemeVariant.forest),
|
||||
home: const Scaffold(
|
||||
body: SizedBox(
|
||||
height: 26,
|
||||
child: MwLauncherStatusBar(status: 'Ready'),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(find.textContaining('v9.8.7'), findsOneWidget);
|
||||
});
|
||||
}
|
||||
|
||||
void _noop() {}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:moonwell_launcher/app/design_system/mw_shells.dart';
|
||||
import 'package:moonwell_launcher/app/launcher_version.dart';
|
||||
import 'package:moonwell_launcher/app/theme/moonwell_design_system.dart';
|
||||
|
||||
void main() {
|
||||
@@ -24,13 +25,16 @@ void main() {
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
theme: MoonWellTheme.create(MoonWellThemeVariant.forest),
|
||||
home: MwLoginShell(
|
||||
form: const SizedBox(),
|
||||
onDrag: _noop,
|
||||
onDoubleTap: _noop,
|
||||
onMinimize: _noop,
|
||||
onMaximizeRestore: _noop,
|
||||
onClose: _noop,
|
||||
home: LauncherVersionScope(
|
||||
version: '9.8.7',
|
||||
child: MwLoginShell(
|
||||
form: const SizedBox(),
|
||||
onDrag: _noop,
|
||||
onDoubleTap: _noop,
|
||||
onMinimize: _noop,
|
||||
onMaximizeRestore: _noop,
|
||||
onClose: _noop,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -38,6 +42,7 @@ void main() {
|
||||
|
||||
final image = tester.widget<Image>(find.byType(Image));
|
||||
expect((image.image as AssetImage).assetName, 'assets/login_key_art.png');
|
||||
expect(find.textContaining('9.8.7'), findsOneWidget);
|
||||
expect(tester.takeException(), isNull);
|
||||
});
|
||||
|
||||
|
||||
+85
-41
@@ -14,6 +14,9 @@ Fails unless the X.Y.Z part of pubspec.yaml matches this value.
|
||||
.PARAMETER ReleaseNotes
|
||||
Text written to the AppCast item description.
|
||||
|
||||
.PARAMETER EnvFile
|
||||
Local dotenv file used to fill environment variables that are not already set.
|
||||
|
||||
.PARAMETER DryRun
|
||||
Builds, signs, and verifies local artifacts without changing production.
|
||||
|
||||
@@ -42,6 +45,7 @@ param(
|
||||
[string]$S3Endpoint,
|
||||
[string]$S3Bucket,
|
||||
[string]$S3Key = 'moonwell_launcher_setup.exe',
|
||||
[string]$EnvFile = '.env',
|
||||
[string]$PrivateKeyPath = '.moonwell_signing\dsa_priv.pem',
|
||||
[string]$PublicKeyPath = 'windows\runner\resources\dsa_pub.pem',
|
||||
[string]$FlutterCommand = 'flutter',
|
||||
@@ -101,6 +105,64 @@ function Get-FileSha256 {
|
||||
return (Get-FileHash -LiteralPath $Path -Algorithm SHA256).Hash.ToLowerInvariant()
|
||||
}
|
||||
|
||||
function Import-DotEnv {
|
||||
param([Parameter(Mandatory = $true)][string]$Path)
|
||||
|
||||
if (-not (Test-Path -LiteralPath $Path -PathType Leaf)) {
|
||||
return
|
||||
}
|
||||
|
||||
$allowedNames = @(
|
||||
'AWS_ACCESS_KEY_ID',
|
||||
'AWS_SECRET_ACCESS_KEY',
|
||||
'AWS_DEFAULT_REGION',
|
||||
'AWS_BUCKET',
|
||||
'AWS_ENDPOINT',
|
||||
'AWS_USE_PATH_STYLE_ENDPOINT',
|
||||
'LAUNCHER_AUTH_KEY'
|
||||
)
|
||||
|
||||
foreach ($line in Get-Content -LiteralPath $Path) {
|
||||
$trimmed = $line.Trim()
|
||||
if (
|
||||
[string]::IsNullOrWhiteSpace($trimmed) -or
|
||||
$trimmed.StartsWith('#')
|
||||
) {
|
||||
continue
|
||||
}
|
||||
|
||||
$separator = $trimmed.IndexOf('=')
|
||||
if ($separator -le 0) {
|
||||
throw "Invalid dotenv line in ${Path}: $line"
|
||||
}
|
||||
|
||||
$name = $trimmed.Substring(0, $separator).Trim()
|
||||
if ($allowedNames -notcontains $name) {
|
||||
continue
|
||||
}
|
||||
|
||||
$value = $trimmed.Substring($separator + 1).Trim()
|
||||
if (
|
||||
$value.Length -ge 2 -and
|
||||
(
|
||||
($value.StartsWith('"') -and $value.EndsWith('"')) -or
|
||||
($value.StartsWith("'") -and $value.EndsWith("'"))
|
||||
)
|
||||
) {
|
||||
$value = $value.Substring(1, $value.Length - 2)
|
||||
}
|
||||
|
||||
$existing = [Environment]::GetEnvironmentVariable($name)
|
||||
if ([string]::IsNullOrWhiteSpace($existing)) {
|
||||
[Environment]::SetEnvironmentVariable(
|
||||
$name,
|
||||
$value,
|
||||
[EnvironmentVariableTarget]::Process
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function Get-AppcastVersion {
|
||||
param([Parameter(Mandatory = $true)][string]$Path)
|
||||
|
||||
@@ -117,6 +179,7 @@ function Write-Appcast {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)][string]$Path,
|
||||
[Parameter(Mandatory = $true)][string]$Version,
|
||||
[Parameter(Mandatory = $true)][string]$BuildVersion,
|
||||
[Parameter(Mandatory = $true)][string]$Notes,
|
||||
[Parameter(Mandatory = $true)][string]$InstallerUrl,
|
||||
[Parameter(Mandatory = $true)][string]$Signature,
|
||||
@@ -167,7 +230,7 @@ function Write-Appcast {
|
||||
'sparkle',
|
||||
'version',
|
||||
$script:SparkleNamespace,
|
||||
$Version
|
||||
$BuildVersion
|
||||
)
|
||||
$writer.WriteAttributeString(
|
||||
'sparkle',
|
||||
@@ -282,6 +345,14 @@ $previousLocation = Get-Location
|
||||
try {
|
||||
Set-Location $repositoryRoot
|
||||
|
||||
$resolvedEnvFile = if ([IO.Path]::IsPathRooted($EnvFile)) {
|
||||
$EnvFile
|
||||
}
|
||||
else {
|
||||
Join-Path $repositoryRoot $EnvFile
|
||||
}
|
||||
Import-DotEnv -Path $resolvedEnvFile
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($S3Endpoint)) {
|
||||
$S3Endpoint = $env:AWS_ENDPOINT
|
||||
}
|
||||
@@ -399,9 +470,10 @@ try {
|
||||
-UseBasicParsing `
|
||||
-OutFile $publishedPath
|
||||
$publishedVersion = Get-AppcastVersion -Path $publishedPath
|
||||
$publishedReleaseVersion = $publishedVersion.Split('+')[0]
|
||||
if (
|
||||
-not [string]::IsNullOrWhiteSpace($publishedVersion) -and
|
||||
[version]$version -le [version]$publishedVersion
|
||||
[version]$version -le [version]$publishedReleaseVersion
|
||||
) {
|
||||
throw (
|
||||
"Version $version is not newer than published version " +
|
||||
@@ -503,6 +575,7 @@ try {
|
||||
'/' +
|
||||
$S3Key
|
||||
)
|
||||
$appcastBuildVersion = "$version+$buildNumber"
|
||||
$appcastPath = if ($DryRun) {
|
||||
Join-Path $temporaryDirectory 'appcast.xml'
|
||||
}
|
||||
@@ -513,6 +586,7 @@ try {
|
||||
Write-Appcast `
|
||||
-Path $appcastPath `
|
||||
-Version $version `
|
||||
-BuildVersion $appcastBuildVersion `
|
||||
-Notes $ReleaseNotes `
|
||||
-InstallerUrl $installerUrl `
|
||||
-Signature $signature `
|
||||
@@ -522,7 +596,7 @@ try {
|
||||
-LiteralPath $appcastPath `
|
||||
-Raw `
|
||||
-Encoding UTF8
|
||||
if ((Get-AppcastVersion -Path $appcastPath) -ne $version) {
|
||||
if ((Get-AppcastVersion -Path $appcastPath) -ne $appcastBuildVersion) {
|
||||
throw 'The generated AppCast contains the wrong version.'
|
||||
}
|
||||
|
||||
@@ -543,45 +617,15 @@ try {
|
||||
$env:MOONWELL_RELEASE_KEY = $S3Key
|
||||
$env:MOONWELL_RELEASE_VERSION = $version
|
||||
$env:MOONWELL_RELEASE_SHA256 = $installerSha256
|
||||
$uploadScript = @'
|
||||
import os
|
||||
import boto3
|
||||
from botocore.config import Config
|
||||
$uploadHelper = Join-Path `
|
||||
$repositoryRoot `
|
||||
'tool\upload_launcher_release.py'
|
||||
if (-not (Test-Path -LiteralPath $uploadHelper -PathType Leaf)) {
|
||||
throw "S3 upload helper was not found: $uploadHelper"
|
||||
}
|
||||
|
||||
path_style = os.getenv("AWS_USE_PATH_STYLE_ENDPOINT", "true").lower()
|
||||
addressing_style = "path" if path_style in ("1", "true", "yes") else "virtual"
|
||||
s3 = boto3.client(
|
||||
"s3",
|
||||
endpoint_url=os.environ["MOONWELL_RELEASE_ENDPOINT"],
|
||||
region_name=os.getenv("AWS_DEFAULT_REGION", "ru-central1"),
|
||||
config=Config(s3={"addressing_style": addressing_style}),
|
||||
)
|
||||
s3.upload_file(
|
||||
os.environ["MOONWELL_RELEASE_FILE"],
|
||||
os.environ["MOONWELL_RELEASE_BUCKET"],
|
||||
os.environ["MOONWELL_RELEASE_KEY"],
|
||||
ExtraArgs={
|
||||
"ACL": "public-read",
|
||||
"ContentType": "application/x-msdownload",
|
||||
"CacheControl": "no-cache, max-age=0",
|
||||
"Metadata": {
|
||||
"launcher-version": os.environ["MOONWELL_RELEASE_VERSION"],
|
||||
"sha256": os.environ["MOONWELL_RELEASE_SHA256"],
|
||||
},
|
||||
},
|
||||
)
|
||||
response = s3.head_object(
|
||||
Bucket=os.environ["MOONWELL_RELEASE_BUCKET"],
|
||||
Key=os.environ["MOONWELL_RELEASE_KEY"],
|
||||
)
|
||||
print("S3_VERSION_ID=" + str(response.get("VersionId", "")))
|
||||
print("S3_CONTENT_LENGTH=" + str(response["ContentLength"]))
|
||||
'@
|
||||
try {
|
||||
Invoke-RequiredCommand -Command $python -Arguments @(
|
||||
'-c',
|
||||
$uploadScript
|
||||
)
|
||||
Invoke-RequiredCommand -Command $python -Arguments @($uploadHelper)
|
||||
}
|
||||
finally {
|
||||
Remove-Item Env:MOONWELL_RELEASE_FILE -ErrorAction SilentlyContinue
|
||||
@@ -651,7 +695,7 @@ print("S3_CONTENT_LENGTH=" + str(response["ContentLength"]))
|
||||
$publishedEnclosure.GetAttribute(
|
||||
'version',
|
||||
$script:SparkleNamespace
|
||||
) -ne $version -or
|
||||
) -ne $appcastBuildVersion -or
|
||||
[long]$publishedEnclosure.length -ne $installer.Length -or
|
||||
$publishedEnclosure.url -ne $installerUrl
|
||||
) {
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
"""Upload a MoonWell Launcher installer to S3-compatible storage.
|
||||
|
||||
Release metadata is supplied by ``deploy_launcher.ps1`` through environment
|
||||
variables. A real Python file avoids Windows PowerShell quoting problems that
|
||||
occur when a multiline program is passed to ``python -c``.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
import boto3
|
||||
from botocore.config import Config
|
||||
|
||||
|
||||
def required_environment_value(name: str) -> str:
|
||||
value = os.environ.get(name, "").strip()
|
||||
if not value:
|
||||
raise RuntimeError(f"Required environment variable is missing: {name}")
|
||||
return value
|
||||
|
||||
|
||||
release_file = Path(required_environment_value("MOONWELL_RELEASE_FILE"))
|
||||
endpoint = required_environment_value("MOONWELL_RELEASE_ENDPOINT")
|
||||
bucket = required_environment_value("MOONWELL_RELEASE_BUCKET")
|
||||
object_key = required_environment_value("MOONWELL_RELEASE_KEY")
|
||||
version = required_environment_value("MOONWELL_RELEASE_VERSION")
|
||||
sha256 = required_environment_value("MOONWELL_RELEASE_SHA256")
|
||||
|
||||
if not release_file.is_file():
|
||||
raise FileNotFoundError(f"Installer does not exist: {release_file}")
|
||||
|
||||
path_style = os.getenv("AWS_USE_PATH_STYLE_ENDPOINT", "true").lower()
|
||||
addressing_style = (
|
||||
"path" if path_style in ("1", "true", "yes") else "virtual"
|
||||
)
|
||||
s3 = boto3.client(
|
||||
"s3",
|
||||
endpoint_url=endpoint,
|
||||
region_name=os.getenv("AWS_DEFAULT_REGION", "ru-central1"),
|
||||
config=Config(s3={"addressing_style": addressing_style}),
|
||||
)
|
||||
|
||||
s3.upload_file(
|
||||
str(release_file),
|
||||
bucket,
|
||||
object_key,
|
||||
ExtraArgs={
|
||||
"ACL": "public-read",
|
||||
"ContentType": "application/x-msdownload",
|
||||
"CacheControl": "no-cache, max-age=0",
|
||||
"Metadata": {
|
||||
"launcher-version": version,
|
||||
"sha256": sha256,
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
response = s3.head_object(Bucket=bucket, Key=object_key)
|
||||
local_size = release_file.stat().st_size
|
||||
remote_size = int(response["ContentLength"])
|
||||
if remote_size != local_size:
|
||||
raise RuntimeError(
|
||||
f"Uploaded object size mismatch: local={local_size}, "
|
||||
f"remote={remote_size}"
|
||||
)
|
||||
|
||||
print("S3_VERSION_ID=" + str(response.get("VersionId", "")))
|
||||
print("S3_CONTENT_LENGTH=" + str(remote_size))
|
||||
@@ -35,6 +35,11 @@ target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX")
|
||||
target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app)
|
||||
target_link_libraries(${BINARY_NAME} PRIVATE "dwmapi.lib")
|
||||
target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}")
|
||||
target_link_options(
|
||||
${BINARY_NAME}
|
||||
PRIVATE
|
||||
"/MANIFESTUAC:level='requireAdministrator' uiAccess='false'"
|
||||
)
|
||||
|
||||
# Run the Flutter tool portions of the build. This must not be removed.
|
||||
add_dependencies(${BINARY_NAME} flutter_assemble)
|
||||
|
||||
Reference in New Issue
Block a user