Initial commit
This commit is contained in:
@@ -0,0 +1,87 @@
|
||||
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[string]$Version,
|
||||
[switch]$SkipFlutterBuild
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
$ProjectRoot = Split-Path -Parent $PSScriptRoot
|
||||
Set-Location $ProjectRoot
|
||||
. (Join-Path $PSScriptRoot 'version-common.ps1')
|
||||
Write-Host "==> Project root: $ProjectRoot" -ForegroundColor Cyan
|
||||
|
||||
if (-not $Version) {
|
||||
$Version = Get-SmPlayerCurrentVersion -Platform windows -ProjectRoot $ProjectRoot
|
||||
}
|
||||
Write-Host "==> Version: $Version" -ForegroundColor Cyan
|
||||
|
||||
$IsccCandidates = @(
|
||||
"$Env:ProgramFiles\Inno Setup 6\ISCC.exe",
|
||||
"${Env:ProgramFiles(x86)}\Inno Setup 6\ISCC.exe",
|
||||
"$Env:LOCALAPPDATA\Programs\Inno Setup 6\ISCC.exe",
|
||||
"$Env:ProgramFiles\Inno Setup 5\ISCC.exe",
|
||||
"${Env:ProgramFiles(x86)}\Inno Setup 5\ISCC.exe",
|
||||
"$Env:LOCALAPPDATA\Programs\Inno Setup 5\ISCC.exe"
|
||||
)
|
||||
$Iscc = $IsccCandidates | Where-Object { Test-Path $_ } | Select-Object -First 1
|
||||
if (-not $Iscc) {
|
||||
$cmd = Get-Command iscc.exe -ErrorAction SilentlyContinue
|
||||
if ($cmd) { $Iscc = $cmd.Source }
|
||||
}
|
||||
if (-not $Iscc) {
|
||||
Write-Host ""
|
||||
Write-Host "[X] ISCC.exe not found." -ForegroundColor Red
|
||||
Write-Host " Install: https://jrsoftware.org/isdl.php" -ForegroundColor Yellow
|
||||
Write-Host " Or: winget install -e --id JRSoftware.InnoSetup" -ForegroundColor Yellow
|
||||
exit 1
|
||||
}
|
||||
Write-Host "==> ISCC: $Iscc" -ForegroundColor Cyan
|
||||
|
||||
if (-not $SkipFlutterBuild) {
|
||||
Write-Host '==> flutter pub get' -ForegroundColor Cyan
|
||||
& flutter pub get
|
||||
if ($LASTEXITCODE -ne 0) { throw "flutter pub get failed (exit $LASTEXITCODE)" }
|
||||
|
||||
$prepareFvpScript = Join-Path $PSScriptRoot 'prepare-fvp-windows-deps.ps1'
|
||||
& powershell -ExecutionPolicy Bypass -File $prepareFvpScript
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "Failed to prepare pinned fvp Windows dependencies (exit $LASTEXITCODE)"
|
||||
}
|
||||
|
||||
$flutterBuildArgs = @('build', 'windows', '--release', '--build-name', $Version)
|
||||
$flutterBuildArgs = Add-SmPlayerVersionDartDefine -Arguments $flutterBuildArgs -Version $Version
|
||||
|
||||
Write-Host "==> flutter $($flutterBuildArgs -join ' ')" -ForegroundColor Cyan
|
||||
$Env:FVP_DEPS_LATEST = $null
|
||||
& flutter @flutterBuildArgs
|
||||
if ($LASTEXITCODE -ne 0) { throw "flutter build failed (exit $LASTEXITCODE)" }
|
||||
} else {
|
||||
Write-Host "==> Skipping flutter build (--SkipFlutterBuild)" -ForegroundColor Yellow
|
||||
}
|
||||
|
||||
$ReleaseDir = Join-Path $ProjectRoot "build\windows\x64\runner\Release"
|
||||
$Exe = Join-Path $ReleaseDir "smplayer.exe"
|
||||
if (-not (Test-Path $Exe)) {
|
||||
throw "Build artifact not found: $Exe (re-run without -SkipFlutterBuild)"
|
||||
}
|
||||
|
||||
$Iss = Join-Path $ProjectRoot "installer\sm_emby.iss"
|
||||
$OutputDir = Join-Path $ProjectRoot "build\installer"
|
||||
New-Item -ItemType Directory -Force -Path $OutputDir | Out-Null
|
||||
|
||||
Write-Host "==> Building installer" -ForegroundColor Cyan
|
||||
& $Iscc "/DAppVersion=$Version" $Iss
|
||||
if ($LASTEXITCODE -ne 0) { throw "ISCC failed (exit $LASTEXITCODE)" }
|
||||
|
||||
$Setup = Join-Path $OutputDir "smplayer-setup-$Version-x64.exe"
|
||||
if (Test-Path $Setup) {
|
||||
$size = (Get-Item $Setup).Length / 1MB
|
||||
Write-Host ""
|
||||
Write-Host "[OK] Installer generated:" -ForegroundColor Green
|
||||
Write-Host " $Setup" -ForegroundColor Green
|
||||
Write-Host (" Size: {0:N2} MB" -f $size) -ForegroundColor Green
|
||||
} else {
|
||||
Write-Host "[!] Expected artifact missing, check $OutputDir" -ForegroundColor Yellow
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
cd "$REPO_ROOT"
|
||||
source "$REPO_ROOT/scripts/version-common.sh"
|
||||
|
||||
APP_VERSION="$(smplayer_current_version macos)"
|
||||
echo "[build-macos] App version: $APP_VERSION (windows/pc line)"
|
||||
|
||||
BUILD_ARGS=(build macos --release)
|
||||
if [[ $# -gt 0 ]]; then
|
||||
BUILD_ARGS+=("$@")
|
||||
fi
|
||||
if ! smplayer_has_flutter_option --build-name "${BUILD_ARGS[@]}"; then
|
||||
BUILD_ARGS+=(--build-name "$APP_VERSION")
|
||||
fi
|
||||
if ! smplayer_has_dart_define SMPLAYER_VERSION "${BUILD_ARGS[@]}"; then
|
||||
BUILD_ARGS+=("--dart-define=SMPLAYER_VERSION=$APP_VERSION")
|
||||
fi
|
||||
|
||||
echo "[build-macos] flutter ${BUILD_ARGS[*]}"
|
||||
exec flutter "${BUILD_ARGS[@]}"
|
||||
Executable
+97
@@ -0,0 +1,97 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
cd "$REPO_ROOT"
|
||||
source "$REPO_ROOT/scripts/version-common.sh"
|
||||
|
||||
if [[ "$(uname -s)" != "Darwin" ]]; then
|
||||
echo "error: this script must run on macOS" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
SKIP_BUILD=0
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
--no-build) SKIP_BUILD=1 ;;
|
||||
-h|--help) sed -n '2,13p' "$0"; exit 0 ;;
|
||||
*) echo "error: unknown arg: $arg" >&2; exit 2 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
VERSION="$(smplayer_current_version macos)"
|
||||
|
||||
APP_DISPLAY_NAME="smPlayer"
|
||||
VOL_NAME="$APP_DISPLAY_NAME"
|
||||
DMG_NAME="${APP_DISPLAY_NAME}-${VERSION}-macos.dmg"
|
||||
APP_SRC="build/macos/Build/Products/Release/sm_emby.app"
|
||||
DIST_DIR="dist"
|
||||
DMG_PATH="${DIST_DIR}/${DMG_NAME}"
|
||||
|
||||
if [[ "$SKIP_BUILD" -eq 0 ]]; then
|
||||
BUILD_ARGS=(build macos --release)
|
||||
if ! smplayer_has_flutter_option --build-name "${BUILD_ARGS[@]}"; then
|
||||
BUILD_ARGS+=(--build-name "$VERSION")
|
||||
fi
|
||||
if ! smplayer_has_dart_define SMPLAYER_VERSION "${BUILD_ARGS[@]}"; then
|
||||
BUILD_ARGS+=("--dart-define=SMPLAYER_VERSION=$VERSION")
|
||||
fi
|
||||
|
||||
echo "==> flutter ${BUILD_ARGS[*]}"
|
||||
flutter "${BUILD_ARGS[@]}"
|
||||
fi
|
||||
|
||||
if [[ ! -d "$APP_SRC" ]]; then
|
||||
echo "error: app bundle not found at $APP_SRC" >&2
|
||||
echo " run without --no-build, or build first: flutter build macos --release" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
STAGE_DIR="$(mktemp -d -t smemby-dmg)"
|
||||
cleanup() {
|
||||
if [[ -n "${MOUNTED_DEV:-}" ]] && hdiutil info | grep -q "$MOUNTED_DEV"; then
|
||||
hdiutil detach "$MOUNTED_DEV" -quiet || true
|
||||
fi
|
||||
rm -rf "$STAGE_DIR"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
echo "==> staging .app at $STAGE_DIR"
|
||||
mkdir -p "$STAGE_DIR/dmg"
|
||||
cp -R "$APP_SRC" "$STAGE_DIR/dmg/${APP_DISPLAY_NAME}.app"
|
||||
ln -s /Applications "$STAGE_DIR/dmg/Applications"
|
||||
|
||||
mkdir -p "$DIST_DIR"
|
||||
rm -f "$DMG_PATH"
|
||||
|
||||
echo "==> hdiutil create $DMG_PATH"
|
||||
hdiutil create \
|
||||
-volname "$VOL_NAME" \
|
||||
-srcfolder "$STAGE_DIR/dmg" \
|
||||
-ov -format UDZO \
|
||||
"$DMG_PATH" >/dev/null
|
||||
|
||||
echo "==> verifying DMG mounts"
|
||||
ATTACH_OUT="$(LANG=C LC_ALL=C hdiutil attach "$DMG_PATH" -nobrowse -readonly -noautoopen)"
|
||||
MOUNTED_DEV="$(printf '%s\n' "$ATTACH_OUT" | awk '/^\/dev\//{print $1; exit}')"
|
||||
MOUNT_POINT="$(printf '%s\n' "$ATTACH_OUT" | awk -F'\t' '/\/Volumes\//{print $NF; exit}')"
|
||||
|
||||
if [[ -z "$MOUNTED_DEV" || -z "$MOUNT_POINT" ]]; then
|
||||
echo "error: failed to parse hdiutil attach output" >&2
|
||||
echo "$ATTACH_OUT" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ ! -d "$MOUNT_POINT/${APP_DISPLAY_NAME}.app" ]] || [[ ! -L "$MOUNT_POINT/Applications" ]]; then
|
||||
echo "error: DMG layout verification failed (missing app or Applications symlink)" >&2
|
||||
hdiutil detach "$MOUNTED_DEV" -quiet || true
|
||||
exit 1
|
||||
fi
|
||||
hdiutil detach "$MOUNTED_DEV" -quiet
|
||||
MOUNTED_DEV=""
|
||||
|
||||
SIZE_HUMAN="$(du -h "$DMG_PATH" | awk '{print $1}')"
|
||||
echo
|
||||
echo "✓ DMG ready: $DMG_PATH ($SIZE_HUMAN)"
|
||||
echo " Unsigned: first launch on another Mac requires right-click -> Open."
|
||||
Executable
+336
@@ -0,0 +1,336 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
|
||||
MEDIA3_TAG='1.10.1'
|
||||
FFMPEG_BRANCH='release/6.0'
|
||||
NDK_PATH="${ANDROID_NDK_HOME:-}"
|
||||
HOST_PLATFORM=''
|
||||
CLEAN=0
|
||||
ANDROID_ABI='21'
|
||||
ENABLED_DECODERS=(dca ac3 eac3 truehd opus vorbis flac)
|
||||
|
||||
usage() {
|
||||
cat <<'USAGE'
|
||||
Usage: scripts/build_media3_ffmpeg.sh [options]
|
||||
|
||||
Options:
|
||||
--clean 编前先清空工作目录
|
||||
--media3-tag <tag> androidx/media tag (default: 1.10.1)
|
||||
--ffmpeg-branch <branch> FFmpeg branch (default: release/6.0)
|
||||
--ndk-path <path> Android NDK 路径(推荐 r27d LTS;默认 $ANDROID_NDK_HOME)
|
||||
--host-platform <name> darwin-x86_64 / linux-x86_64
|
||||
(省略时根据 uname 推断;Apple Silicon 强烈建议
|
||||
通过 Docker linux-x86_64 容器执行)
|
||||
-h, --help 显示本帮助
|
||||
USAGE
|
||||
}
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--clean) CLEAN=1; shift ;;
|
||||
--media3-tag) MEDIA3_TAG="$2"; shift 2 ;;
|
||||
--ffmpeg-branch) FFMPEG_BRANCH="$2"; shift 2 ;;
|
||||
--ndk-path) NDK_PATH="$2"; shift 2 ;;
|
||||
--host-platform) HOST_PLATFORM="$2"; shift 2 ;;
|
||||
-h|--help) usage; exit 0 ;;
|
||||
*) echo "unknown option: $1" >&2; usage; exit 2 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
|
||||
require_cmd() {
|
||||
if ! command -v "$1" >/dev/null 2>&1; then
|
||||
echo "ERROR: $1 not found in PATH" >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
require_cmd git
|
||||
require_cmd shasum
|
||||
|
||||
if [[ -z "${NDK_PATH}" ]]; then
|
||||
echo 'ERROR: NDK path not specified. Use --ndk-path or set ANDROID_NDK_HOME.' >&2
|
||||
echo 'Pin to NDK r27d LTS (27.3.13750724) — ABI-compatible with Media3 1.10.1 build_ffmpeg.sh.' >&2
|
||||
exit 1
|
||||
fi
|
||||
if [[ ! -d "${NDK_PATH}" ]]; then
|
||||
echo "ERROR: NDK path not a directory: ${NDK_PATH}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
SDK_PATH="${ANDROID_HOME:-${ANDROID_SDK_ROOT:-}}"
|
||||
if [[ -z "${SDK_PATH}" ]]; then
|
||||
for candidate in \
|
||||
"${HOME}/Library/Android/sdk" \
|
||||
"${HOME}/Android/Sdk" \
|
||||
"/opt/android-sdk" \
|
||||
"/usr/local/share/android-sdk"; do
|
||||
if [[ -d "${candidate}/platforms" ]]; then
|
||||
SDK_PATH="${candidate}"
|
||||
echo "[detect] Android SDK at ${SDK_PATH}"
|
||||
break
|
||||
fi
|
||||
done
|
||||
fi
|
||||
if [[ -z "${SDK_PATH}" || ! -d "${SDK_PATH}/platforms" ]]; then
|
||||
echo 'ERROR: Android SDK not found. Set ANDROID_HOME or install Android Studio SDK.' >&2
|
||||
echo ' Required: $ANDROID_HOME/platforms (android-34+) + build-tools.' >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -z "${HOST_PLATFORM}" ]]; then
|
||||
uname_s="$(uname -s)"
|
||||
uname_m="$(uname -m)"
|
||||
case "${uname_s}-${uname_m}" in
|
||||
Darwin-x86_64) HOST_PLATFORM='darwin-x86_64' ;;
|
||||
Darwin-arm64)
|
||||
HOST_PLATFORM='darwin-x86_64'
|
||||
echo 'WARN: host is Apple Silicon; Media3 README only documents darwin-x86_64.' >&2
|
||||
echo ' Build will proceed under Rosetta. For reproducibility prefer a' >&2
|
||||
echo ' Docker linux-x86_64 container (see plugins/media3_engine/README.md).' >&2
|
||||
;;
|
||||
Linux-x86_64) HOST_PLATFORM='linux-x86_64' ;;
|
||||
*) echo "ERROR: unsupported host ${uname_s}-${uname_m}. Pass --host-platform." >&2; exit 1 ;;
|
||||
esac
|
||||
fi
|
||||
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
||||
WORK_ROOT="${REPO_ROOT}/.media3-build"
|
||||
MEDIA_REPO="${WORK_ROOT}/media"
|
||||
FFMPEG_MODULE_PATH="${MEDIA_REPO}/libraries/decoder_ffmpeg/src/main"
|
||||
FFMPEG_DIR="${FFMPEG_MODULE_PATH}/jni/ffmpeg"
|
||||
OUT_LIBS_DIR="${REPO_ROOT}/android/app/libs"
|
||||
AAR_NAME="media3-decoder-ffmpeg-${MEDIA3_TAG}-arm64.aar"
|
||||
OUT_AAR="${OUT_LIBS_DIR}/${AAR_NAME}"
|
||||
PATCH_BACKUP_DIR=''
|
||||
PATCHED_FILES=()
|
||||
|
||||
backup_patch_file() {
|
||||
local file="$1"
|
||||
if [[ -z "${PATCH_BACKUP_DIR}" ]]; then
|
||||
PATCH_BACKUP_DIR="$(mktemp -d)"
|
||||
fi
|
||||
local backup="${PATCH_BACKUP_DIR}/${#PATCHED_FILES[@]}"
|
||||
cp "${file}" "${backup}"
|
||||
PATCHED_FILES+=("${file}:${backup}")
|
||||
}
|
||||
|
||||
restore_patched_files() {
|
||||
local entry file backup
|
||||
for entry in "${PATCHED_FILES[@]}"; do
|
||||
file="${entry%%:*}"
|
||||
backup="${entry#*:}"
|
||||
if [[ -f "${backup}" ]]; then
|
||||
cp "${backup}" "${file}"
|
||||
fi
|
||||
done
|
||||
if [[ -n "${PATCH_BACKUP_DIR}" ]]; then
|
||||
rm -rf "${PATCH_BACKUP_DIR}"
|
||||
fi
|
||||
}
|
||||
trap restore_patched_files EXIT
|
||||
|
||||
if [[ ${CLEAN} -eq 1 && -d "${WORK_ROOT}" ]]; then
|
||||
echo "[clean] removing ${WORK_ROOT}"
|
||||
rm -rf "${WORK_ROOT}"
|
||||
fi
|
||||
mkdir -p "${WORK_ROOT}" "${OUT_LIBS_DIR}"
|
||||
|
||||
|
||||
if [[ ! -d "${MEDIA_REPO}/.git" ]]; then
|
||||
echo "[clone] androidx/media @ ${MEDIA3_TAG}"
|
||||
git clone --depth 1 --branch "${MEDIA3_TAG}" \
|
||||
https://github.com/androidx/media.git "${MEDIA_REPO}"
|
||||
else
|
||||
echo "[skip] ${MEDIA_REPO} already exists; pass --clean to refetch"
|
||||
fi
|
||||
|
||||
|
||||
if [[ ! -d "${FFMPEG_DIR}/.git" ]]; then
|
||||
echo "[clone] FFmpeg ${FFMPEG_BRANCH}"
|
||||
mkdir -p "$(dirname "${FFMPEG_DIR}")"
|
||||
git clone --depth 1 --branch "${FFMPEG_BRANCH}" \
|
||||
https://git.ffmpeg.org/ffmpeg.git "${FFMPEG_DIR}"
|
||||
else
|
||||
echo "[skip] ${FFMPEG_DIR} already exists"
|
||||
fi
|
||||
|
||||
|
||||
JNI_BUILD_SH="${FFMPEG_MODULE_PATH}/jni/build_ffmpeg.sh"
|
||||
if [[ ! -f "${JNI_BUILD_SH}" ]]; then
|
||||
echo "ERROR: ${JNI_BUILD_SH} not found (androidx/media tag ${MEDIA3_TAG} 内未包含)" >&2
|
||||
exit 1
|
||||
fi
|
||||
if ! grep -q '# SM-PATCH arm64-only' "${JNI_BUILD_SH}"; then
|
||||
echo "[patch] forcing arm64-v8a only in ${JNI_BUILD_SH##*/}"
|
||||
backup_patch_file "${JNI_BUILD_SH}"
|
||||
python3 - "${JNI_BUILD_SH}" <<'PY' || { echo 'ERROR: failed to patch build_ffmpeg.sh (upstream layout changed?)' >&2; exit 1; }
|
||||
import re, sys
|
||||
p = sys.argv[1]
|
||||
src = open(p, 'r', encoding='utf-8').read()
|
||||
if '# SM-PATCH arm64-only' in src:
|
||||
sys.exit(0)
|
||||
KEEP = 'arm64-v8a'
|
||||
block_re = re.compile(
|
||||
r'\./configure\s*\\\s*\n'
|
||||
r'(?:.*\n)*?'
|
||||
r'\s*--libdir=android-libs/([\w-]+)\s*\\\s*\n'
|
||||
r'(?:.*\n)*?'
|
||||
r'make clean\s*\n',
|
||||
re.M,
|
||||
)
|
||||
removed = []
|
||||
chunks = []
|
||||
last = 0
|
||||
for m in block_re.finditer(src):
|
||||
abi = m.group(1)
|
||||
if abi == KEEP:
|
||||
continue
|
||||
chunks.append(src[last:m.start()])
|
||||
last = m.end()
|
||||
removed.append(abi)
|
||||
chunks.append(src[last:])
|
||||
if not removed:
|
||||
sys.stderr.write('ERROR: no non-arm64 configure blocks found; upstream layout may have changed.\n')
|
||||
sys.exit(2)
|
||||
result = ''.join(chunks).replace(
|
||||
'set -eu\n',
|
||||
f'set -eu\n# SM-PATCH arm64-only (removed: {",".join(removed)})\n',
|
||||
1,
|
||||
)
|
||||
open(p, 'w', encoding='utf-8').write(result)
|
||||
sys.stderr.write(f'[ok] removed ABI blocks: {",".join(removed)}\n')
|
||||
PY
|
||||
fi
|
||||
|
||||
|
||||
echo "[build] FFmpeg native libs (decoders: ${ENABLED_DECODERS[*]})"
|
||||
pushd "${FFMPEG_MODULE_PATH}/jni" >/dev/null
|
||||
NDK_PATH="${NDK_PATH}" \
|
||||
HOST_PLATFORM="${HOST_PLATFORM}" \
|
||||
ANDROID_ABI="${ANDROID_ABI}" \
|
||||
./build_ffmpeg.sh \
|
||||
"${FFMPEG_MODULE_PATH}" \
|
||||
"${NDK_PATH}" \
|
||||
"${HOST_PLATFORM}" \
|
||||
"${ANDROID_ABI}" \
|
||||
"${ENABLED_DECODERS[@]}"
|
||||
popd >/dev/null
|
||||
|
||||
|
||||
echo '[build] Gradle :libraries:decoder_ffmpeg:assembleRelease'
|
||||
cat >"${MEDIA_REPO}/local.properties" <<EOF
|
||||
sdk.dir=${SDK_PATH}
|
||||
ndk.dir=${NDK_PATH}
|
||||
EOF
|
||||
|
||||
NDK_VERSION="$(awk -F'= *' '/^Pkg\.Revision/{print $2}' "${NDK_PATH}/source.properties" | tr -d '\r')"
|
||||
if [[ -z "${NDK_VERSION}" ]]; then
|
||||
echo "ERROR: cannot read Pkg.Revision from ${NDK_PATH}/source.properties" >&2
|
||||
exit 1
|
||||
fi
|
||||
COMMON_CONFIG="${MEDIA_REPO}/common_config.gradle"
|
||||
if ! grep -q '// SM-PATCH ndkVersion' "${COMMON_CONFIG}"; then
|
||||
echo "[patch] injecting ndkVersion=${NDK_VERSION} into common_config.gradle"
|
||||
backup_patch_file "${COMMON_CONFIG}"
|
||||
python3 - "${COMMON_CONFIG}" "${NDK_VERSION}" <<'PY' || { echo 'ERROR: failed to inject ndkVersion' >&2; exit 1; }
|
||||
import re, sys
|
||||
p, version = sys.argv[1], sys.argv[2]
|
||||
src = open(p, 'r', encoding='utf-8').read()
|
||||
patched, n = re.subn(
|
||||
r'^android\s*\{\s*\n',
|
||||
f'android {{\n ndkVersion = "{version}" // SM-PATCH ndkVersion\n',
|
||||
src, count=1, flags=re.M,
|
||||
)
|
||||
if n != 1:
|
||||
sys.stderr.write('ERROR: failed to find `android {` in common_config.gradle\n')
|
||||
sys.exit(2)
|
||||
open(p, 'w', encoding='utf-8').write(patched)
|
||||
PY
|
||||
fi
|
||||
|
||||
DECODER_BUILD_GRADLE="${MEDIA_REPO}/libraries/decoder_ffmpeg/build.gradle"
|
||||
if ! grep -q '// SM-PATCH abiFilters' "${DECODER_BUILD_GRADLE}"; then
|
||||
echo "[patch] restricting decoder_ffmpeg abiFilters to arm64-v8a"
|
||||
backup_patch_file "${DECODER_BUILD_GRADLE}"
|
||||
python3 - "${DECODER_BUILD_GRADLE}" <<'PY' || { echo 'ERROR: failed to inject abiFilters' >&2; exit 1; }
|
||||
import re, sys
|
||||
p = sys.argv[1]
|
||||
src = open(p, 'r', encoding='utf-8').read()
|
||||
inject = (
|
||||
"android {\n"
|
||||
" defaultConfig {\n"
|
||||
" ndk {\n"
|
||||
" abiFilters 'arm64-v8a' // SM-PATCH abiFilters\n"
|
||||
" }\n"
|
||||
" }\n"
|
||||
"}\n\n"
|
||||
)
|
||||
patched, n = re.subn(
|
||||
r'(apply from: "\$gradle\.ext\.androidxMediaSettingsDir/common_config\.gradle"\n)',
|
||||
r'\1\n' + inject,
|
||||
src, count=1,
|
||||
)
|
||||
if n != 1:
|
||||
sys.stderr.write('ERROR: failed to find common_config apply line in decoder_ffmpeg/build.gradle\n')
|
||||
sys.exit(2)
|
||||
open(p, 'w', encoding='utf-8').write(patched)
|
||||
PY
|
||||
fi
|
||||
|
||||
pushd "${MEDIA_REPO}" >/dev/null
|
||||
ANDROID_HOME="${SDK_PATH}" ANDROID_SDK_ROOT="${SDK_PATH}" \
|
||||
./gradlew --no-daemon :lib-decoder-ffmpeg:assembleRelease
|
||||
popd >/dev/null
|
||||
|
||||
|
||||
GRADLE_AAR=''
|
||||
for pattern in \
|
||||
"${MEDIA_REPO}/libraries/decoder_ffmpeg/buildout/outputs/aar/lib-decoder-ffmpeg-release.aar" \
|
||||
"${MEDIA_REPO}/libraries/decoder_ffmpeg/build/outputs/aar/lib-decoder-ffmpeg-release.aar" \
|
||||
"${MEDIA_REPO}/libraries/decoder_ffmpeg/buildout/outputs/aar/decoder_ffmpeg-release.aar" \
|
||||
"${MEDIA_REPO}/libraries/decoder_ffmpeg/build/outputs/aar/decoder_ffmpeg-release.aar"; do
|
||||
if [[ -f "${pattern}" ]]; then
|
||||
GRADLE_AAR="${pattern}"; break
|
||||
fi
|
||||
done
|
||||
if [[ -z "${GRADLE_AAR}" ]]; then
|
||||
echo 'ERROR: AAR not found under decoder_ffmpeg/{buildout,build}/outputs/aar/. 检查 Gradle 输出' >&2
|
||||
exit 1
|
||||
fi
|
||||
cp "${GRADLE_AAR}" "${OUT_AAR}"
|
||||
SHA256="$(shasum -a 256 "${OUT_AAR}" | awk '{print $1}')"
|
||||
|
||||
|
||||
CHECKSUMS="${OUT_LIBS_DIR}/CHECKSUMS.txt"
|
||||
touch "${CHECKSUMS}"
|
||||
python3 - "${CHECKSUMS}" "${AAR_NAME}" "${SHA256}" <<'PY'
|
||||
import sys, re
|
||||
p, name, sha = sys.argv[1], sys.argv[2], sys.argv[3]
|
||||
with open(p, 'r', encoding='utf-8') as f:
|
||||
lines = [ln for ln in f.read().splitlines() if not ln.endswith(name)]
|
||||
lines.append(f'{sha} {name}')
|
||||
with open(p, 'w', encoding='utf-8') as f:
|
||||
f.write('\n'.join(lines) + '\n')
|
||||
PY
|
||||
|
||||
|
||||
cat <<INFO
|
||||
|
||||
[done] media3-decoder-ffmpeg AAR build complete.
|
||||
|
||||
output : ${OUT_AAR}
|
||||
size : $(wc -c <"${OUT_AAR}") bytes
|
||||
sha256 : ${SHA256}
|
||||
tag : ${MEDIA3_TAG}
|
||||
ffmpeg : ${FFMPEG_BRANCH}
|
||||
ndk : ${NDK_PATH}
|
||||
|
||||
下一步:
|
||||
1. 核对 SHA-256 是否与团队约定一致
|
||||
2. git add ${OUT_AAR##*/Desktop/cc-env/player/sm-emby/} android/app/libs/CHECKSUMS.txt
|
||||
3. cd ${REPO_ROOT} && flutter build apk --debug 验证 AAR 加载
|
||||
INFO
|
||||
@@ -0,0 +1,3 @@
|
||||
$projectRoot = Split-Path -Parent $PSScriptRoot
|
||||
& dart (Join-Path $PSScriptRoot 'dev_android.dart') --project-root $projectRoot @args
|
||||
exit $LASTEXITCODE
|
||||
Executable
+5
@@ -0,0 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
exec dart "$REPO_ROOT/scripts/dev_android.dart" --project-root "$REPO_ROOT" "$@"
|
||||
@@ -0,0 +1,21 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
cd "$REPO_ROOT"
|
||||
source "$REPO_ROOT/scripts/version-common.sh"
|
||||
|
||||
APP_VERSION="$(smplayer_current_version macos)"
|
||||
echo "[dev-macos] App version: $APP_VERSION (windows/pc line)"
|
||||
|
||||
RUN_ARGS=(run -d macos)
|
||||
if [[ $# -gt 0 ]]; then
|
||||
RUN_ARGS+=("$@")
|
||||
fi
|
||||
if ! smplayer_has_dart_define SMPLAYER_VERSION "${RUN_ARGS[@]}"; then
|
||||
RUN_ARGS+=("--dart-define=SMPLAYER_VERSION=$APP_VERSION")
|
||||
fi
|
||||
|
||||
echo "[dev-macos] flutter ${RUN_ARGS[*]}"
|
||||
exec flutter "${RUN_ARGS[@]}"
|
||||
@@ -0,0 +1,152 @@
|
||||
# SM-Emby Dev Run Helper
|
||||
|
||||
$Device = 'windows'
|
||||
$Force = $false
|
||||
$Offline = $false
|
||||
$RefreshFvpDeps = $false
|
||||
$SkipDefenderCheck = $false
|
||||
$FlutterArgs = @()
|
||||
|
||||
for ($i = 0; $i -lt $args.Count; $i++) {
|
||||
$arg = [string]$args[$i]
|
||||
switch ($arg) {
|
||||
'-Device' {
|
||||
$i++
|
||||
if ($i -ge $args.Count) { throw 'Missing value for -Device' }
|
||||
$Device = [string]$args[$i]
|
||||
continue
|
||||
}
|
||||
'-Force' { $Force = $true; continue }
|
||||
'-Offline' { $Offline = $true; continue }
|
||||
'-RefreshFvpDeps' { $RefreshFvpDeps = $true; continue }
|
||||
'-SkipDefenderCheck' { $SkipDefenderCheck = $true; continue }
|
||||
'-FlutterArgs' {
|
||||
if ($i + 1 -lt $args.Count) { $FlutterArgs = @($args[($i + 1)..($args.Count - 1)]) }
|
||||
$i = $args.Count
|
||||
continue
|
||||
}
|
||||
'--' {
|
||||
if ($i + 1 -lt $args.Count) { $FlutterArgs = @($args[($i + 1)..($args.Count - 1)]) }
|
||||
$i = $args.Count
|
||||
continue
|
||||
}
|
||||
default { throw "Unknown argument: $arg" }
|
||||
}
|
||||
}
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
$repoRoot = Split-Path -Parent $PSScriptRoot
|
||||
Set-Location $repoRoot
|
||||
. (Join-Path $PSScriptRoot 'version-common.ps1')
|
||||
|
||||
$pubspec = Join-Path $repoRoot 'pubspec.yaml'
|
||||
$pubspecLock = Join-Path $repoRoot 'pubspec.lock'
|
||||
$packageConfig = Join-Path $repoRoot '.dart_tool/package_config.json'
|
||||
$pubGetStamp = Join-Path $repoRoot '.dart_tool/.dev-run-pubget.stamp'
|
||||
|
||||
function Get-PubDepsFingerprint {
|
||||
$parts = @()
|
||||
foreach ($file in @($pubspec, $pubspecLock)) {
|
||||
if (Test-Path $file) {
|
||||
$parts += (Get-FileHash -Path $file -Algorithm SHA256).Hash
|
||||
} else {
|
||||
$parts += 'missing'
|
||||
}
|
||||
}
|
||||
return ($parts -join ':')
|
||||
}
|
||||
|
||||
function Need-PubGet {
|
||||
if ($Force) { return $true }
|
||||
if (-not (Test-Path $packageConfig)) { return $true }
|
||||
if (-not (Test-Path $pubspec)) { return $true }
|
||||
if (-not (Test-Path $pubGetStamp)) { return $true }
|
||||
|
||||
$previousFingerprint = (Get-Content -Path $pubGetStamp -Raw -ErrorAction SilentlyContinue).Trim()
|
||||
return ($previousFingerprint -ne (Get-PubDepsFingerprint))
|
||||
}
|
||||
|
||||
function Save-PubGetStamp {
|
||||
Set-Content -Path $pubGetStamp -Value (Get-PubDepsFingerprint) -NoNewline -Encoding ASCII
|
||||
}
|
||||
|
||||
function Disable-FvpLatestForDevRun {
|
||||
if ($Device.ToLowerInvariant() -ne 'windows') { return }
|
||||
if (-not $Env:FVP_DEPS_LATEST) { return }
|
||||
|
||||
Write-Host '[dev-run] 清理 FVP_DEPS_LATEST,使用项目固定版本的 fvp/mdk-sdk' -ForegroundColor Green
|
||||
$Env:FVP_DEPS_LATEST = $null
|
||||
}
|
||||
|
||||
function Prepare-PinnedFvpWindowsDependencies {
|
||||
if ($Device.ToLowerInvariant() -ne 'windows') { return }
|
||||
|
||||
$prepareScript = Join-Path $PSScriptRoot 'prepare-fvp-windows-deps.ps1'
|
||||
$prepareArguments = @('-ExecutionPolicy', 'Bypass', '-File', $prepareScript)
|
||||
if ($RefreshFvpDeps) { $prepareArguments += '-ForceRefresh' }
|
||||
|
||||
& powershell @prepareArguments
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "Failed to prepare pinned fvp Windows dependencies (exit $LASTEXITCODE)"
|
||||
}
|
||||
}
|
||||
|
||||
function Test-DefenderExclusion {
|
||||
if ($SkipDefenderCheck) { return }
|
||||
if ($Device.ToLowerInvariant() -ne 'windows') { return }
|
||||
|
||||
try {
|
||||
$pref = Get-MpPreference -ErrorAction Stop
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
|
||||
$root = $repoRoot.TrimEnd('\').ToLowerInvariant()
|
||||
$covered = $false
|
||||
foreach ($ex in @($pref.ExclusionPath)) {
|
||||
if ([string]::IsNullOrWhiteSpace($ex)) { continue }
|
||||
$e = $ex.TrimEnd('\').ToLowerInvariant()
|
||||
if ($root -eq $e -or $root.StartsWith($e + '\')) { $covered = $true; break }
|
||||
}
|
||||
|
||||
if ($covered) {
|
||||
Write-Host '[dev-run] Windows Defender 已排除项目目录,原生编译不受实时扫描拖慢' -ForegroundColor Green
|
||||
return
|
||||
}
|
||||
|
||||
Write-Host '[dev-run] 提示: 项目目录未加入 Windows Defender 排除,原生(fvp/libmdk)编译可能被实时扫描显著拖慢' -ForegroundColor Yellow
|
||||
Write-Host ' 用【管理员】PowerShell 执行一次即可(之后走缓存):' -ForegroundColor Yellow
|
||||
Write-Host (' Add-MpPreference -ExclusionPath "' + $repoRoot + '"') -ForegroundColor DarkYellow
|
||||
Write-Host ' 加 -SkipDefenderCheck 可跳过本提示' -ForegroundColor DarkGray
|
||||
}
|
||||
|
||||
$sw = [System.Diagnostics.Stopwatch]::StartNew()
|
||||
Disable-FvpLatestForDevRun
|
||||
Test-DefenderExclusion
|
||||
|
||||
if (Need-PubGet) {
|
||||
Write-Host '[dev-run] 依赖有变更 -> flutter pub get' -ForegroundColor Yellow
|
||||
$pubArgs = @('pub', 'get')
|
||||
if ($Offline) { $pubArgs += '--offline' }
|
||||
& flutter @pubArgs
|
||||
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
|
||||
Save-PubGetStamp
|
||||
} else {
|
||||
Write-Host '[dev-run] 依赖未变更 -> 跳过 pub get' -ForegroundColor Green
|
||||
}
|
||||
|
||||
Prepare-PinnedFvpWindowsDependencies
|
||||
|
||||
$appVersion = Get-SmPlayerCurrentVersion -Platform windows -ProjectRoot $repoRoot
|
||||
Write-Host "[dev-run] App version: $appVersion (windows/pc line)" -ForegroundColor Cyan
|
||||
|
||||
$runArgs = @('run', '-d', $Device, '--no-pub')
|
||||
if ($FlutterArgs) { $runArgs += $FlutterArgs }
|
||||
$runArgs = Add-SmPlayerVersionDartDefine -Arguments $runArgs -Version $appVersion
|
||||
|
||||
Write-Host "[dev-run] flutter $($runArgs -join ' ')" -ForegroundColor Cyan
|
||||
Write-Host "[dev-run] 启动准备耗时: $([int]$sw.Elapsed.TotalMilliseconds) ms" -ForegroundColor DarkGray
|
||||
|
||||
& flutter @runArgs
|
||||
exit $LASTEXITCODE
|
||||
+142
@@ -0,0 +1,142 @@
|
||||
|
||||
$Command = 'help'
|
||||
$Rest = @()
|
||||
if ($args.Count -gt 0) {
|
||||
$Command = [string]$args[0]
|
||||
if ($args.Count -gt 1) {
|
||||
$Rest = @($args[1..($args.Count - 1)])
|
||||
}
|
||||
}
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
$RepoRoot = Split-Path -Parent $PSScriptRoot
|
||||
$ScriptsDir = $PSScriptRoot
|
||||
$PinnedMdkReleaseUrl = 'https://github.com/wang-bin/mdk-sdk/releases/download/v0.37.0'
|
||||
Set-Location $RepoRoot
|
||||
. (Join-Path $ScriptsDir 'version-common.ps1')
|
||||
|
||||
function Show-Help {
|
||||
@"
|
||||
SM-Emby Unified Dev Entry
|
||||
|
||||
scripts\dev.ps1 <subcommand> [args...]
|
||||
|
||||
Subcommands:
|
||||
run Smart dev run -- prepare the pinned Windows MDK SDK and skip unchanged pub get
|
||||
pass-through args: -Device, -Force, -Offline, -RefreshFvpDeps, -FlutterArgs <flutter args>
|
||||
android Android dev run -- 自动检测真机/模拟器
|
||||
pass-through args: -Device <id>, -Emulator <id>, -Force, -Offline, -FlutterArgs <flutter args>
|
||||
installer Build .exe installer (alias of build-installer.ps1)
|
||||
pass-through args: -SkipFlutterBuild, -Version <x.y.z>
|
||||
build Build Windows release with the project-pinned MDK SDK
|
||||
build-apk Build Android arm64 release with the project-pinned MDK SDK
|
||||
gen dart run build_runner build --delete-conflicting-outputs
|
||||
clean flutter clean
|
||||
pub flutter pub get
|
||||
analyze flutter analyze
|
||||
doctor flutter doctor -v
|
||||
help This help
|
||||
|
||||
Examples:
|
||||
scripts\dev.ps1 run
|
||||
scripts\dev.ps1 run -Force -FlutterArgs --release
|
||||
scripts\dev.ps1 run -RefreshFvpDeps
|
||||
scripts\dev.ps1 android
|
||||
scripts\dev.ps1 android -Device 18fa8f58
|
||||
scripts\dev.ps1 android -Emulator Pixel_10_Pro_XL
|
||||
scripts\dev.ps1 installer
|
||||
scripts\dev.ps1 installer -SkipFlutterBuild
|
||||
scripts\dev.ps1 build
|
||||
scripts\dev.ps1 build-apk
|
||||
scripts\dev.ps1 gen
|
||||
"@ | Write-Host
|
||||
}
|
||||
|
||||
function Invoke-Sub([string]$ScriptName) {
|
||||
$path = Join-Path $ScriptsDir $ScriptName
|
||||
if (-not (Test-Path $path)) { throw "Script not found: $path" }
|
||||
& $path @args
|
||||
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
|
||||
}
|
||||
|
||||
function Invoke-Flutter([string[]]$Args) {
|
||||
Write-Host "[dev] flutter $($Args -join ' ')" -ForegroundColor Cyan
|
||||
& flutter @Args
|
||||
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
|
||||
}
|
||||
|
||||
function Prepare-PinnedFvpWindowsDependencies {
|
||||
Invoke-Flutter @('pub', 'get')
|
||||
|
||||
$prepareScript = Join-Path $ScriptsDir 'prepare-fvp-windows-deps.ps1'
|
||||
& powershell -ExecutionPolicy Bypass -File $prepareScript
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "Failed to prepare pinned fvp Windows dependencies (exit $LASTEXITCODE)"
|
||||
}
|
||||
}
|
||||
|
||||
function Prepare-PinnedFvpAndroidDependencies {
|
||||
$Env:FVP_DEPS_URL = $PinnedMdkReleaseUrl
|
||||
$Env:FVP_DEPS_LATEST = $null
|
||||
|
||||
Invoke-Flutter @('pub', 'get')
|
||||
|
||||
$prepareScript = Join-Path $ScriptsDir 'prepare_fvp_android_deps.dart'
|
||||
& dart $prepareScript --project-root $RepoRoot
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "Failed to prepare pinned fvp Android dependencies (exit $LASTEXITCODE)"
|
||||
}
|
||||
}
|
||||
|
||||
switch ($Command.ToLower()) {
|
||||
'run' { Invoke-Sub 'dev-run.ps1' @Rest }
|
||||
'android' { Invoke-Sub 'dev-android.ps1' @Rest }
|
||||
'installer' { Invoke-Sub 'build-installer.ps1' @Rest }
|
||||
'build' {
|
||||
Prepare-PinnedFvpWindowsDependencies
|
||||
|
||||
$appVersion = Get-SmPlayerCurrentVersion -Platform windows -ProjectRoot $RepoRoot
|
||||
Write-Host "[dev] App version: $appVersion (windows/pc line)" -ForegroundColor Cyan
|
||||
|
||||
$flutterBuildArgs = @('build', 'windows', '--release')
|
||||
if ($Rest) { $flutterBuildArgs += $Rest }
|
||||
$flutterBuildArgs = Add-SmPlayerBuildName -Arguments $flutterBuildArgs -Version $appVersion
|
||||
$flutterBuildArgs = Add-SmPlayerVersionDartDefine -Arguments $flutterBuildArgs -Version $appVersion
|
||||
Invoke-Flutter $flutterBuildArgs
|
||||
}
|
||||
'build-apk' {
|
||||
Prepare-PinnedFvpAndroidDependencies
|
||||
|
||||
$appVersion = Get-SmPlayerCurrentVersion -Platform android -ProjectRoot $RepoRoot
|
||||
$buildNumber = ConvertTo-SmPlayerBuildNumber -Version $appVersion
|
||||
Write-Host "[dev] App version: $appVersion (android line), build number: $buildNumber" -ForegroundColor Cyan
|
||||
|
||||
$flutterBuildArgs = @('build', 'apk', '--release', '--target-platform', 'android-arm64')
|
||||
if ($Rest) { $flutterBuildArgs += $Rest }
|
||||
$flutterBuildArgs = Add-SmPlayerBuildName -Arguments $flutterBuildArgs -Version $appVersion
|
||||
if (-not (Test-SmPlayerFlutterOptionPresent -Arguments $flutterBuildArgs -OptionName '--build-number')) {
|
||||
$flutterBuildArgs += @('--build-number', [string]$buildNumber)
|
||||
}
|
||||
$flutterBuildArgs = Add-SmPlayerVersionDartDefine -Arguments $flutterBuildArgs -Version $appVersion
|
||||
Invoke-Flutter $flutterBuildArgs
|
||||
}
|
||||
'gen' {
|
||||
Write-Host '[dev] dart run build_runner build --delete-conflicting-outputs' -ForegroundColor Cyan
|
||||
& dart run build_runner build --delete-conflicting-outputs
|
||||
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
|
||||
}
|
||||
'clean' { Invoke-Flutter @('clean') }
|
||||
'pub' { Invoke-Flutter @('pub', 'get') }
|
||||
'analyze' { Invoke-Flutter @('analyze') }
|
||||
'doctor' { Invoke-Flutter @('doctor', '-v') }
|
||||
'help' { Show-Help }
|
||||
'--help' { Show-Help }
|
||||
'-h' { Show-Help }
|
||||
default {
|
||||
Write-Host "Unknown subcommand: $Command" -ForegroundColor Red
|
||||
Write-Host ''
|
||||
Show-Help
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,471 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
const _fvpDepsUrl =
|
||||
'https://github.com/wang-bin/mdk-sdk/releases/download/v0.37.0';
|
||||
const _androidApplicationId = 'com.smplayer.smplayer';
|
||||
const _debugApplicationId = '$_androidApplicationId.dev';
|
||||
const _maxEmulatorWait = Duration(seconds: 120);
|
||||
const _pollInterval = Duration(seconds: 3);
|
||||
|
||||
Future<void> main(List<String> arguments) async {
|
||||
try {
|
||||
final options = _Options.parse(arguments);
|
||||
if (options.help) {
|
||||
stdout.writeln(_usage);
|
||||
return;
|
||||
}
|
||||
exitCode = await _run(options);
|
||||
} on FormatException catch (error) {
|
||||
stderr.writeln('error: ${error.message}');
|
||||
stderr.writeln(_usage);
|
||||
exitCode = 2;
|
||||
} on ProcessException catch (error) {
|
||||
stderr.writeln('error: $error');
|
||||
exitCode = 1;
|
||||
} on StateError catch (error) {
|
||||
stderr.writeln('error: ${error.message}');
|
||||
exitCode = 1;
|
||||
}
|
||||
}
|
||||
|
||||
Future<int> _run(_Options options) async {
|
||||
final stopwatch = Stopwatch()..start();
|
||||
final projectRoot = options.projectRoot ?? _defaultProjectRoot;
|
||||
final environment = _flutterEnvironment();
|
||||
var deviceId = options.device;
|
||||
|
||||
if (deviceId != null) {
|
||||
stdout.writeln('[dev-android] 使用指定设备: $deviceId');
|
||||
} else {
|
||||
deviceId = await _androidDevice(projectRoot, environment);
|
||||
if (deviceId == null) {
|
||||
final emulatorId =
|
||||
options.emulator ?? await _firstEmulator(projectRoot, environment);
|
||||
if (emulatorId == null) {
|
||||
stderr
|
||||
..writeln('[dev-android] 未找到 Android 设备,请:')
|
||||
..writeln(' 1. 连接真机(开启 USB 调试)')
|
||||
..writeln(
|
||||
' 2. 或创建模拟器: '
|
||||
'flutter emulators --create --name MyEmulator',
|
||||
)
|
||||
..writeln(' 也可用 --device <id> 直接指定设备');
|
||||
return 1;
|
||||
}
|
||||
stdout
|
||||
..writeln('[dev-android] 启动模拟器: $emulatorId')
|
||||
..writeln('[dev-android] 等待模拟器启动...');
|
||||
await Process.start(
|
||||
'flutter',
|
||||
['emulators', '--launch', emulatorId],
|
||||
workingDirectory: projectRoot,
|
||||
environment: environment,
|
||||
includeParentEnvironment: false,
|
||||
runInShell: Platform.isWindows,
|
||||
mode: ProcessStartMode.detached,
|
||||
);
|
||||
deviceId = await _waitForDevice(projectRoot, environment);
|
||||
if (deviceId == null) {
|
||||
stderr.writeln(
|
||||
'[dev-android] 模拟器启动超时 '
|
||||
'(${_maxEmulatorWait.inSeconds}s),请手动启动后重试',
|
||||
);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
stdout.writeln('[dev-android] 目标设备: $deviceId');
|
||||
}
|
||||
|
||||
if (_needsPubGet(projectRoot, options.force)) {
|
||||
stdout.writeln('[dev-android] 依赖有变更 -> flutter pub get');
|
||||
final pubExit = await _inheritFlutter(
|
||||
['pub', 'get', if (options.offline) '--offline'],
|
||||
projectRoot,
|
||||
environment,
|
||||
);
|
||||
if (pubExit != 0) return pubExit;
|
||||
} else {
|
||||
stdout.writeln('[dev-android] 依赖未变更 -> 跳过 pub get');
|
||||
}
|
||||
|
||||
final prepareFvpExit = await _prepareFvpAndroidDependencies(
|
||||
projectRoot,
|
||||
environment,
|
||||
);
|
||||
if (prepareFvpExit != 0) return prepareFvpExit;
|
||||
|
||||
final appVersion = await _appVersion(projectRoot);
|
||||
final versionBuildNumber = await _appBuildNumber(projectRoot, appVersion);
|
||||
final installedBuildNumber = await _installedAppBuildNumber(
|
||||
projectRoot,
|
||||
deviceId,
|
||||
environment,
|
||||
);
|
||||
final appBuildNumber =
|
||||
installedBuildNumber != null && installedBuildNumber > versionBuildNumber
|
||||
? installedBuildNumber
|
||||
: versionBuildNumber;
|
||||
stdout.writeln(
|
||||
'[dev-android] App version: $appVersion (android line), '
|
||||
'build number: $appBuildNumber',
|
||||
);
|
||||
if (installedBuildNumber != null &&
|
||||
installedBuildNumber > versionBuildNumber) {
|
||||
stdout.writeln(
|
||||
'[dev-android] 设备已安装更高版本号 $installedBuildNumber,'
|
||||
'开发包沿用该版本号以避免降级重装',
|
||||
);
|
||||
}
|
||||
environment['SMPLAYER_ANDROID_VERSION_CODE'] = '$appBuildNumber';
|
||||
final runArguments = [
|
||||
'run',
|
||||
'-d',
|
||||
deviceId,
|
||||
'--no-pub',
|
||||
...options.flutterArguments,
|
||||
];
|
||||
if (!_hasDartDefine(runArguments, 'SMPLAYER_VERSION')) {
|
||||
runArguments.add('--dart-define=SMPLAYER_VERSION=$appVersion');
|
||||
}
|
||||
|
||||
stdout
|
||||
..writeln('[dev-android] flutter ${runArguments.join(' ')}')
|
||||
..writeln('[dev-android] 启动准备耗时: ${stopwatch.elapsedMilliseconds} ms');
|
||||
return _inheritFlutter(runArguments, projectRoot, environment);
|
||||
}
|
||||
|
||||
Future<int> _prepareFvpAndroidDependencies(
|
||||
String projectRoot,
|
||||
Map<String, String> environment,
|
||||
) async {
|
||||
final prepareScript = File(
|
||||
'${File.fromUri(Platform.script).parent.path}'
|
||||
'${Platform.pathSeparator}prepare_fvp_android_deps.dart',
|
||||
);
|
||||
final result = await Process.run(
|
||||
Platform.resolvedExecutable,
|
||||
[prepareScript.path, '--project-root', projectRoot],
|
||||
workingDirectory: projectRoot,
|
||||
environment: environment,
|
||||
includeParentEnvironment: false,
|
||||
);
|
||||
stdout.write(result.stdout);
|
||||
stderr.write(result.stderr);
|
||||
return result.exitCode;
|
||||
}
|
||||
|
||||
Map<String, String> _flutterEnvironment() {
|
||||
final environment = Map<String, String>.of(Platform.environment)
|
||||
..['FVP_DEPS_URL'] = _fvpDepsUrl
|
||||
..remove('FVP_DEPS_LATEST');
|
||||
if (!Platform.isWindows) {
|
||||
if ((environment['FLUTTER_STORAGE_BASE_URL'] ?? '').isEmpty) {
|
||||
environment['FLUTTER_STORAGE_BASE_URL'] = 'https://storage.flutter-io.cn';
|
||||
}
|
||||
if ((environment['PUB_HOSTED_URL'] ?? '').isEmpty) {
|
||||
environment['PUB_HOSTED_URL'] = 'https://pub.flutter-io.cn';
|
||||
}
|
||||
}
|
||||
return environment;
|
||||
}
|
||||
|
||||
bool _needsPubGet(String projectRoot, bool force) {
|
||||
if (force) return true;
|
||||
final packageConfig = File(
|
||||
'$projectRoot${Platform.pathSeparator}.dart_tool'
|
||||
'${Platform.pathSeparator}package_config.json',
|
||||
);
|
||||
final pubspec = File('$projectRoot${Platform.pathSeparator}pubspec.yaml');
|
||||
if (!packageConfig.existsSync() || !pubspec.existsSync()) return true;
|
||||
final configTime = packageConfig.lastModifiedSync();
|
||||
if (pubspec.lastModifiedSync().isAfter(configTime)) return true;
|
||||
final lock = File('$projectRoot${Platform.pathSeparator}pubspec.lock');
|
||||
return lock.existsSync() && lock.lastModifiedSync().isAfter(configTime);
|
||||
}
|
||||
|
||||
Future<String?> _androidDevice(
|
||||
String projectRoot,
|
||||
Map<String, String> environment,
|
||||
) async {
|
||||
final result = await _flutter(
|
||||
['devices', '--machine'],
|
||||
projectRoot,
|
||||
environment,
|
||||
);
|
||||
if (result.exitCode != 0) return null;
|
||||
try {
|
||||
final devices = jsonDecode('${result.stdout}');
|
||||
if (devices is! List) return null;
|
||||
for (final device in devices) {
|
||||
if (device is Map &&
|
||||
'${device['targetPlatform']}'.startsWith('android')) {
|
||||
final id = '${device['id']}';
|
||||
if (id.isNotEmpty && id != 'null') return id;
|
||||
}
|
||||
}
|
||||
} on FormatException {
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
Future<String?> _firstEmulator(
|
||||
String projectRoot,
|
||||
Map<String, String> environment,
|
||||
) async {
|
||||
final result = await _flutter(['emulators'], projectRoot, environment);
|
||||
if (result.exitCode != 0) return null;
|
||||
final emulatorPattern = RegExp(
|
||||
r'^\s*(\S+)\s+.*\bandroid\b',
|
||||
caseSensitive: false,
|
||||
);
|
||||
for (final line in '${result.stdout}'.split(RegExp(r'\r?\n'))) {
|
||||
final match = emulatorPattern.firstMatch(line);
|
||||
if (match != null) return match[1];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
Future<String?> _waitForDevice(
|
||||
String projectRoot,
|
||||
Map<String, String> environment,
|
||||
) async {
|
||||
for (
|
||||
var waited = _pollInterval;
|
||||
waited <= _maxEmulatorWait;
|
||||
waited += _pollInterval
|
||||
) {
|
||||
await Future<void>.delayed(_pollInterval);
|
||||
final deviceId = await _androidDevice(projectRoot, environment);
|
||||
if (deviceId != null) return deviceId;
|
||||
stdout.writeln('[dev-android] 等待中... (${waited.inSeconds}s)');
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
Future<String> _appVersion(String projectRoot) async {
|
||||
final result = await _runVersionCommand(projectRoot, ['current', 'android']);
|
||||
return '${result.stdout}'.trim();
|
||||
}
|
||||
|
||||
Future<int> _appBuildNumber(String projectRoot, String appVersion) async {
|
||||
final result = await _runVersionCommand(projectRoot, [
|
||||
'build-number',
|
||||
appVersion,
|
||||
]);
|
||||
final buildNumber = int.tryParse('${result.stdout}'.trim());
|
||||
if (buildNumber == null || buildNumber <= 0) {
|
||||
throw StateError('Invalid Android build number: ${result.stdout}');
|
||||
}
|
||||
return buildNumber;
|
||||
}
|
||||
|
||||
Future<int?> _installedAppBuildNumber(
|
||||
String projectRoot,
|
||||
String deviceId,
|
||||
Map<String, String> environment,
|
||||
) async {
|
||||
final adbExecutable = _androidDebugBridgeExecutable(projectRoot, environment);
|
||||
try {
|
||||
final result = await Process.run(
|
||||
adbExecutable,
|
||||
['-s', deviceId, 'shell', 'dumpsys', 'package', _debugApplicationId],
|
||||
workingDirectory: projectRoot,
|
||||
environment: environment,
|
||||
includeParentEnvironment: false,
|
||||
runInShell: Platform.isWindows,
|
||||
);
|
||||
if (result.exitCode != 0) return null;
|
||||
final versionCodeMatch = RegExp(
|
||||
r'\bversionCode=(\d+)\b',
|
||||
).firstMatch('${result.stdout}');
|
||||
return versionCodeMatch == null
|
||||
? null
|
||||
: int.tryParse(versionCodeMatch.group(1)!);
|
||||
} on ProcessException {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
String _androidDebugBridgeExecutable(
|
||||
String projectRoot,
|
||||
Map<String, String> environment,
|
||||
) {
|
||||
final sdkDirectories = <String?>[
|
||||
environment['ANDROID_SDK_ROOT'],
|
||||
environment['ANDROID_HOME'],
|
||||
_androidSdkDirectoryFromLocalProperties(projectRoot),
|
||||
if (Platform.isWindows && environment['LOCALAPPDATA'] != null)
|
||||
'${environment['LOCALAPPDATA']}${Platform.pathSeparator}Android'
|
||||
'${Platform.pathSeparator}sdk',
|
||||
];
|
||||
final executableName = Platform.isWindows ? 'adb.exe' : 'adb';
|
||||
for (final sdkDirectory in sdkDirectories) {
|
||||
if (sdkDirectory == null || sdkDirectory.trim().isEmpty) continue;
|
||||
final executable = File(
|
||||
'${sdkDirectory.trim()}${Platform.pathSeparator}platform-tools'
|
||||
'${Platform.pathSeparator}$executableName',
|
||||
);
|
||||
if (executable.existsSync()) return executable.path;
|
||||
}
|
||||
return executableName;
|
||||
}
|
||||
|
||||
String? _androidSdkDirectoryFromLocalProperties(String projectRoot) {
|
||||
final propertiesFile = File(
|
||||
'$projectRoot${Platform.pathSeparator}android'
|
||||
'${Platform.pathSeparator}local.properties',
|
||||
);
|
||||
if (!propertiesFile.existsSync()) return null;
|
||||
for (final line in propertiesFile.readAsLinesSync()) {
|
||||
final trimmedLine = line.trim();
|
||||
if (!trimmedLine.startsWith('sdk.dir=')) continue;
|
||||
return trimmedLine
|
||||
.substring('sdk.dir='.length)
|
||||
.replaceAll(r'\:', ':')
|
||||
.replaceAll(r'\\', r'\');
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
Future<ProcessResult> _runVersionCommand(
|
||||
String projectRoot,
|
||||
List<String> arguments,
|
||||
) async {
|
||||
final result = await Process.run(Platform.resolvedExecutable, [
|
||||
'${File.fromUri(Platform.script).parent.path}'
|
||||
'${Platform.pathSeparator}version.dart',
|
||||
...arguments,
|
||||
'--project-root',
|
||||
projectRoot,
|
||||
]);
|
||||
if (result.exitCode != 0) {
|
||||
throw StateError('${result.stderr}'.trim());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
bool _hasDartDefine(List<String> arguments, String name) {
|
||||
var previous = '';
|
||||
for (final argument in arguments) {
|
||||
if (argument.startsWith('--dart-define=$name=') ||
|
||||
(previous == '--dart-define' && argument.startsWith('$name='))) {
|
||||
return true;
|
||||
}
|
||||
previous = argument;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
Future<ProcessResult> _flutter(
|
||||
List<String> arguments,
|
||||
String projectRoot,
|
||||
Map<String, String> environment,
|
||||
) {
|
||||
return Process.run(
|
||||
'flutter',
|
||||
arguments,
|
||||
workingDirectory: projectRoot,
|
||||
environment: environment,
|
||||
includeParentEnvironment: false,
|
||||
runInShell: Platform.isWindows,
|
||||
);
|
||||
}
|
||||
|
||||
Future<int> _inheritFlutter(
|
||||
List<String> arguments,
|
||||
String projectRoot,
|
||||
Map<String, String> environment,
|
||||
) async {
|
||||
final process = await Process.start(
|
||||
'flutter',
|
||||
arguments,
|
||||
workingDirectory: projectRoot,
|
||||
environment: environment,
|
||||
includeParentEnvironment: false,
|
||||
runInShell: Platform.isWindows,
|
||||
mode: ProcessStartMode.inheritStdio,
|
||||
);
|
||||
return process.exitCode;
|
||||
}
|
||||
|
||||
final class _Options {
|
||||
const _Options({
|
||||
this.device,
|
||||
this.emulator,
|
||||
this.projectRoot,
|
||||
this.force = false,
|
||||
this.offline = false,
|
||||
this.help = false,
|
||||
this.flutterArguments = const [],
|
||||
});
|
||||
|
||||
factory _Options.parse(List<String> arguments) {
|
||||
String? device;
|
||||
String? emulator;
|
||||
String? projectRoot;
|
||||
var force = false;
|
||||
var offline = false;
|
||||
var help = false;
|
||||
var flutterArguments = <String>[];
|
||||
|
||||
for (var index = 0; index < arguments.length; index++) {
|
||||
final argument = arguments[index];
|
||||
switch (argument.toLowerCase()) {
|
||||
case '--device' || '-device':
|
||||
device = _valueAfter(arguments, ++index, argument);
|
||||
case '--emulator' || '-emulator':
|
||||
emulator = _valueAfter(arguments, ++index, argument);
|
||||
case '--project-root':
|
||||
projectRoot = _valueAfter(arguments, ++index, argument);
|
||||
case '--force' || '-force':
|
||||
force = true;
|
||||
case '--offline' || '-offline':
|
||||
offline = true;
|
||||
case '-h' || '--help':
|
||||
help = true;
|
||||
case '--' || '-flutterargs':
|
||||
flutterArguments = arguments.skip(index + 1).toList();
|
||||
index = arguments.length;
|
||||
default:
|
||||
throw FormatException("Unknown argument '$argument'.");
|
||||
}
|
||||
}
|
||||
return _Options(
|
||||
device: device,
|
||||
emulator: emulator,
|
||||
projectRoot: projectRoot,
|
||||
force: force,
|
||||
offline: offline,
|
||||
help: help,
|
||||
flutterArguments: flutterArguments,
|
||||
);
|
||||
}
|
||||
|
||||
final String? device;
|
||||
final String? emulator;
|
||||
final String? projectRoot;
|
||||
final bool force;
|
||||
final bool offline;
|
||||
final bool help;
|
||||
final List<String> flutterArguments;
|
||||
}
|
||||
|
||||
String _valueAfter(List<String> arguments, int index, String option) {
|
||||
if (index >= arguments.length) {
|
||||
throw FormatException('Missing value for $option.');
|
||||
}
|
||||
return arguments[index];
|
||||
}
|
||||
|
||||
String get _defaultProjectRoot =>
|
||||
File.fromUri(Platform.script).parent.parent.path;
|
||||
|
||||
const _usage = '''Usage:
|
||||
dart scripts/dev_android.dart [--device <id>] [--emulator <id>]
|
||||
[--force] [--offline] [-- <flutter run args>]
|
||||
|
||||
PowerShell aliases -Device, -Emulator, -Force, -Offline, and -FlutterArgs
|
||||
remain supported.''';
|
||||
@@ -0,0 +1,34 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
KEEP_TAGS=(
|
||||
"flutter:V"
|
||||
"ExoPlayerImpl:V"
|
||||
"MediaCodecVideoRenderer:V"
|
||||
"MediaCodecRenderer:V"
|
||||
"MediaCodecInfo:V"
|
||||
"VideoCapabilities:V"
|
||||
"Media3Engine:V"
|
||||
"Media3PlatformView:V"
|
||||
"Media3Player:V"
|
||||
"Media3PlayerEngine:V"
|
||||
"Media3PlayerInstance:V"
|
||||
"DolbyVisionConfig:V"
|
||||
"HevcReader:V"
|
||||
"Mp4Extractor:I"
|
||||
"DefaultTrackSelector:I"
|
||||
"EventLogger:V"
|
||||
"AndroidRuntime:E"
|
||||
)
|
||||
|
||||
RAW=0
|
||||
if [[ "${1:-}" == "--raw" ]]; then
|
||||
RAW=1
|
||||
fi
|
||||
|
||||
if [[ $RAW -eq 0 ]]; then
|
||||
adb logcat -c
|
||||
fi
|
||||
|
||||
exec adb logcat "${KEEP_TAGS[@]}" "*:S"
|
||||
@@ -0,0 +1,219 @@
|
||||
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[string]$Url = '',
|
||||
[switch]$ForceRefresh
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
|
||||
$MdkVersion = '0.37.0'
|
||||
|
||||
function Test-SevenZipArchive {
|
||||
param([string]$Path)
|
||||
if (-not (Test-Path $Path)) { return $false }
|
||||
$stream = [System.IO.File]::OpenRead($Path)
|
||||
try {
|
||||
if ($stream.Length -lt 6) { return $false }
|
||||
$buffer = New-Object byte[] 6
|
||||
[void]$stream.Read($buffer, 0, 6)
|
||||
} finally {
|
||||
$stream.Dispose()
|
||||
}
|
||||
$magic = 0x37, 0x7A, 0xBC, 0xAF, 0x27, 0x1C
|
||||
for ($i = 0; $i -lt 6; $i++) {
|
||||
if ($buffer[$i] -ne $magic[$i]) { return $false }
|
||||
}
|
||||
return $true
|
||||
}
|
||||
|
||||
function Resolve-CMakeExecutable {
|
||||
$pathCommand = Get-Command 'cmake.exe' -ErrorAction SilentlyContinue
|
||||
if ($pathCommand) {
|
||||
return $pathCommand.Source
|
||||
}
|
||||
|
||||
$visualStudioInstallations = @()
|
||||
$vswherePath = Join-Path ${Env:ProgramFiles(x86)} 'Microsoft Visual Studio\Installer\vswhere.exe'
|
||||
if (Test-Path $vswherePath) {
|
||||
$visualStudioInstallations += @(
|
||||
& $vswherePath -products '*' -property installationPath 2>$null
|
||||
)
|
||||
}
|
||||
|
||||
foreach ($programFilesRoot in @($Env:ProgramFiles, ${Env:ProgramFiles(x86)})) {
|
||||
if (-not $programFilesRoot) { continue }
|
||||
foreach ($edition in @('BuildTools', 'Community', 'Professional', 'Enterprise')) {
|
||||
$visualStudioInstallations += Join-Path $programFilesRoot "Microsoft Visual Studio\2022\$edition"
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($installationPath in $visualStudioInstallations | Select-Object -Unique) {
|
||||
if (-not $installationPath) { continue }
|
||||
$candidate = Join-Path $installationPath 'Common7\IDE\CommonExtensions\Microsoft\CMake\CMake\bin\cmake.exe'
|
||||
if (Test-Path $candidate) {
|
||||
return $candidate
|
||||
}
|
||||
}
|
||||
|
||||
$androidSdkRoots = @(
|
||||
$Env:ANDROID_SDK_ROOT,
|
||||
$Env:ANDROID_HOME,
|
||||
$(if ($Env:LOCALAPPDATA) { Join-Path $Env:LOCALAPPDATA 'Android\sdk' })
|
||||
) | Where-Object { $_ -and (Test-Path $_) } | Select-Object -Unique
|
||||
|
||||
foreach ($androidSdkRoot in $androidSdkRoots) {
|
||||
$cmakeRoot = Join-Path $androidSdkRoot 'cmake'
|
||||
if (-not (Test-Path $cmakeRoot)) { continue }
|
||||
$candidate = Get-ChildItem $cmakeRoot -Directory -ErrorAction SilentlyContinue |
|
||||
Sort-Object Name -Descending |
|
||||
ForEach-Object { Join-Path $_.FullName 'bin\cmake.exe' } |
|
||||
Where-Object { Test-Path $_ } |
|
||||
Select-Object -First 1
|
||||
if ($candidate) {
|
||||
return $candidate
|
||||
}
|
||||
}
|
||||
|
||||
return $null
|
||||
}
|
||||
|
||||
$ProjectRoot = Split-Path -Parent $PSScriptRoot
|
||||
Set-Location $ProjectRoot
|
||||
|
||||
$PackageConfig = Join-Path $ProjectRoot '.dart_tool\package_config.json'
|
||||
if (-not (Test-Path $PackageConfig)) {
|
||||
throw "package_config.json not found: $PackageConfig (run flutter pub get first)"
|
||||
}
|
||||
|
||||
$config = Get-Content $PackageConfig -Raw | ConvertFrom-Json
|
||||
$fvp = $config.packages | Where-Object { $_.name -eq 'fvp' } | Select-Object -First 1
|
||||
if (-not $fvp) {
|
||||
throw 'Package fvp not found in .dart_tool/package_config.json'
|
||||
}
|
||||
|
||||
$fvpRoot = [Uri]$fvp.rootUri
|
||||
if (-not $fvpRoot.IsFile) {
|
||||
throw "Unsupported fvp rootUri: $($fvp.rootUri)"
|
||||
}
|
||||
|
||||
$fvpWindowsDir = Join-Path $fvpRoot.LocalPath 'windows'
|
||||
if (-not (Test-Path $fvpWindowsDir)) {
|
||||
throw "fvp windows directory not found: $fvpWindowsDir"
|
||||
}
|
||||
|
||||
$sdkDir = Join-Path $fvpWindowsDir 'mdk-sdk'
|
||||
$sdkVersionMarkerPath = Join-Path $sdkDir '.smplayer-mdk-version'
|
||||
$archivePath = Join-Path $fvpWindowsDir "mdk-sdk-windows-x64-v$MdkVersion-vs2026.7z"
|
||||
|
||||
if ($ForceRefresh) {
|
||||
Write-Host "==> Force refreshing fvp mdk-sdk v$MdkVersion" -ForegroundColor Yellow
|
||||
if (Test-Path $sdkDir) {
|
||||
Remove-Item $sdkDir -Recurse -Force
|
||||
}
|
||||
if (Test-Path $archivePath) {
|
||||
Remove-Item $archivePath -Force
|
||||
}
|
||||
}
|
||||
|
||||
if (Test-Path $sdkDir) {
|
||||
$installedMdkVersion = if (Test-Path $sdkVersionMarkerPath) {
|
||||
(Get-Content $sdkVersionMarkerPath -Raw).Trim()
|
||||
} else {
|
||||
''
|
||||
}
|
||||
if ($installedMdkVersion -eq $MdkVersion) {
|
||||
Write-Host "==> fvp mdk-sdk v$MdkVersion already exists: $sdkDir" -ForegroundColor Green
|
||||
exit 0
|
||||
}
|
||||
|
||||
$displayVersion = if ($installedMdkVersion) { $installedMdkVersion } else { 'unknown' }
|
||||
Write-Host "==> Replacing cached mdk-sdk v$displayVersion with v$MdkVersion" -ForegroundColor Yellow
|
||||
Remove-Item $sdkDir -Recurse -Force
|
||||
}
|
||||
|
||||
$candidates = @()
|
||||
if ($Url) {
|
||||
$candidates += $Url
|
||||
} else {
|
||||
$candidates += "https://github.com/wang-bin/mdk-sdk/releases/download/v$MdkVersion/mdk-sdk-windows-x64-vs2026.7z"
|
||||
}
|
||||
|
||||
if ((Test-Path $archivePath) -and -not (Test-SevenZipArchive $archivePath)) {
|
||||
Write-Host "==> Cached archive is not a valid 7z, removing: $archivePath" -ForegroundColor Yellow
|
||||
Remove-Item $archivePath -Force
|
||||
}
|
||||
|
||||
if (-not (Test-Path $archivePath)) {
|
||||
$downloaded = $false
|
||||
:outer foreach ($candidate in $candidates) {
|
||||
$maxAttempts = 3
|
||||
for ($attempt = 1; $attempt -le $maxAttempts; $attempt++) {
|
||||
Write-Host "==> Downloading mdk-sdk (attempt $attempt/$maxAttempts): $candidate" -ForegroundColor Cyan
|
||||
try {
|
||||
Invoke-WebRequest -Uri $candidate -OutFile $archivePath `
|
||||
-UseBasicParsing -MaximumRedirection 10 `
|
||||
-UserAgent 'Mozilla/5.0' -TimeoutSec 120
|
||||
} catch {
|
||||
Write-Host " download failed: $($_.Exception.Message)" -ForegroundColor Yellow
|
||||
if (Test-Path $archivePath) { Remove-Item $archivePath -Force }
|
||||
if ($attempt -lt $maxAttempts) {
|
||||
$delay = $attempt * 10
|
||||
Write-Host " retrying in ${delay}s..." -ForegroundColor Yellow
|
||||
Start-Sleep -Seconds $delay
|
||||
}
|
||||
continue
|
||||
}
|
||||
if (Test-SevenZipArchive $archivePath) {
|
||||
$downloaded = $true
|
||||
break outer
|
||||
}
|
||||
$size = (Get-Item $archivePath).Length
|
||||
Write-Host " not a valid 7z archive (size=$size bytes), trying next" -ForegroundColor Yellow
|
||||
Remove-Item $archivePath -Force
|
||||
break # bad content, skip retries for this URL
|
||||
}
|
||||
}
|
||||
if (-not $downloaded) {
|
||||
throw "Failed to download a valid mdk-sdk archive from: $($candidates -join ', ')"
|
||||
}
|
||||
}
|
||||
|
||||
Write-Host "==> Extracting mdk-sdk to: $fvpWindowsDir" -ForegroundColor Cyan
|
||||
|
||||
$sevenZipCommand = Get-Command '7z.exe' -ErrorAction SilentlyContinue
|
||||
$sevenZip = if ($sevenZipCommand) {
|
||||
$sevenZipCommand.Source
|
||||
} else {
|
||||
@(
|
||||
"$Env:ProgramFiles\7-Zip\7z.exe",
|
||||
"${Env:ProgramFiles(x86)}\7-Zip\7z.exe"
|
||||
) | Where-Object { Test-Path $_ } | Select-Object -First 1
|
||||
}
|
||||
|
||||
if ($sevenZip) {
|
||||
& $sevenZip x '-y' "-o$fvpWindowsDir" $archivePath
|
||||
} else {
|
||||
$cmakeExecutable = Resolve-CMakeExecutable
|
||||
if (-not $cmakeExecutable) {
|
||||
throw 'Neither 7-Zip nor CMake was found. Install the Visual Studio C++ CMake tools or 7-Zip, then retry.'
|
||||
}
|
||||
Write-Host "==> Using CMake: $cmakeExecutable" -ForegroundColor DarkGray
|
||||
Push-Location $fvpWindowsDir
|
||||
try {
|
||||
& $cmakeExecutable -E tar xvf $archivePath
|
||||
} finally {
|
||||
Pop-Location
|
||||
}
|
||||
}
|
||||
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "Failed to extract mdk-sdk (exit $LASTEXITCODE)"
|
||||
}
|
||||
|
||||
if (-not (Test-Path $sdkDir)) {
|
||||
throw "mdk-sdk not found after extraction: $sdkDir"
|
||||
}
|
||||
|
||||
Set-Content -Path $sdkVersionMarkerPath -Value $MdkVersion -NoNewline
|
||||
Write-Host "==> fvp mdk-sdk v$MdkVersion ready: $sdkDir" -ForegroundColor Green
|
||||
@@ -0,0 +1,130 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
const _pinnedMdkVersion = '0.37.0';
|
||||
|
||||
Future<void> main(List<String> arguments) async {
|
||||
try {
|
||||
final projectRoot = _parseProjectRoot(arguments);
|
||||
await _invalidateStaleFvpAndroidSdk(projectRoot);
|
||||
} on FormatException catch (error) {
|
||||
stderr.writeln('error: ${error.message}');
|
||||
exitCode = 2;
|
||||
} on FileSystemException catch (error) {
|
||||
stderr.writeln('error: ${error.message}');
|
||||
exitCode = 1;
|
||||
} on StateError catch (error) {
|
||||
stderr.writeln('error: ${error.message}');
|
||||
exitCode = 1;
|
||||
}
|
||||
}
|
||||
|
||||
String _parseProjectRoot(List<String> arguments) {
|
||||
if (arguments.isEmpty) return _defaultProjectRoot;
|
||||
if (arguments.length == 2 && arguments.first == '--project-root') {
|
||||
return Directory(arguments.last).absolute.path;
|
||||
}
|
||||
throw const FormatException(
|
||||
'Usage: dart scripts/prepare_fvp_android_deps.dart '
|
||||
'[--project-root <path>]',
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _invalidateStaleFvpAndroidSdk(String projectRoot) async {
|
||||
final packageConfigFile = File(
|
||||
'$projectRoot${Platform.pathSeparator}.dart_tool'
|
||||
'${Platform.pathSeparator}package_config.json',
|
||||
);
|
||||
if (!await packageConfigFile.exists()) {
|
||||
throw StateError(
|
||||
'package_config.json not found: ${packageConfigFile.path} '
|
||||
'(run flutter pub get first)',
|
||||
);
|
||||
}
|
||||
|
||||
final packageConfig = jsonDecode(await packageConfigFile.readAsString());
|
||||
if (packageConfig is! Map<String, dynamic>) {
|
||||
throw StateError('Invalid package_config.json root object');
|
||||
}
|
||||
final packages = packageConfig['packages'];
|
||||
if (packages is! List) {
|
||||
throw StateError('Invalid package_config.json packages list');
|
||||
}
|
||||
|
||||
Map<String, dynamic>? fvpPackage;
|
||||
for (final package in packages) {
|
||||
if (package is Map<String, dynamic> && package['name'] == 'fvp') {
|
||||
fvpPackage = package;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (fvpPackage == null) {
|
||||
throw StateError('Package fvp not found in package_config.json');
|
||||
}
|
||||
|
||||
final rootUriValue = fvpPackage['rootUri'];
|
||||
if (rootUriValue is! String || rootUriValue.isEmpty) {
|
||||
throw StateError('Package fvp has an invalid rootUri');
|
||||
}
|
||||
final fvpRootUri = packageConfigFile.uri.resolve(rootUriValue);
|
||||
if (fvpRootUri.scheme != 'file') {
|
||||
throw StateError('Unsupported fvp rootUri: $rootUriValue');
|
||||
}
|
||||
|
||||
final fvpRootDirectory = Directory.fromUri(fvpRootUri);
|
||||
final fvpAndroidDirectory = Directory(
|
||||
'${fvpRootDirectory.path}${Platform.pathSeparator}android',
|
||||
);
|
||||
if (!await fvpAndroidDirectory.exists()) {
|
||||
throw StateError(
|
||||
'fvp Android directory not found: ${fvpAndroidDirectory.path}',
|
||||
);
|
||||
}
|
||||
|
||||
final sdkDirectory = Directory(
|
||||
'${fvpAndroidDirectory.path}${Platform.pathSeparator}mdk-sdk',
|
||||
);
|
||||
final sdkArchive = File(
|
||||
'${fvpAndroidDirectory.path}${Platform.pathSeparator}mdk-sdk-android.7z',
|
||||
);
|
||||
final versionMarker = File(
|
||||
'${fvpAndroidDirectory.path}'
|
||||
'${Platform.pathSeparator}.smplayer-mdk-version',
|
||||
);
|
||||
final findMdkFile = File(
|
||||
'${sdkDirectory.path}${Platform.pathSeparator}lib'
|
||||
'${Platform.pathSeparator}cmake'
|
||||
'${Platform.pathSeparator}FindMDK.cmake',
|
||||
);
|
||||
|
||||
final installedVersion = await versionMarker.exists()
|
||||
? (await versionMarker.readAsString()).trim()
|
||||
: '';
|
||||
final sdkIsReady =
|
||||
installedVersion == _pinnedMdkVersion && await findMdkFile.exists();
|
||||
if (sdkIsReady) {
|
||||
stdout.writeln(
|
||||
'[fvp-android] mdk-sdk v$_pinnedMdkVersion is ready: '
|
||||
'${sdkDirectory.path}',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
final displayVersion = installedVersion.isEmpty
|
||||
? 'unknown'
|
||||
: installedVersion;
|
||||
stdout.writeln(
|
||||
'[fvp-android] Invalidating cached mdk-sdk v$displayVersion; '
|
||||
'the next Flutter build will download v$_pinnedMdkVersion.',
|
||||
);
|
||||
if (await sdkDirectory.exists()) {
|
||||
await sdkDirectory.delete(recursive: true);
|
||||
}
|
||||
if (await sdkArchive.exists()) {
|
||||
await sdkArchive.delete();
|
||||
}
|
||||
await versionMarker.writeAsString(_pinnedMdkVersion, flush: true);
|
||||
}
|
||||
|
||||
String get _defaultProjectRoot =>
|
||||
File.fromUri(Platform.script).parent.parent.absolute.path;
|
||||
@@ -0,0 +1,25 @@
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[ValidateSet('pc', 'windows', 'android', 'both')]
|
||||
[string]$Platform,
|
||||
[switch]$DryRun,
|
||||
[switch]$NoPush,
|
||||
[string]$Remote = 'origin'
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$projectRoot = Split-Path -Parent $PSScriptRoot
|
||||
$arguments = @(
|
||||
(Join-Path $PSScriptRoot 'version.dart'),
|
||||
'tag',
|
||||
$Platform,
|
||||
'--project-root',
|
||||
$projectRoot,
|
||||
'--remote',
|
||||
$Remote
|
||||
)
|
||||
if ($DryRun) { $arguments += '--dry-run' }
|
||||
if ($NoPush) { $arguments += '--no-push' }
|
||||
|
||||
& dart @arguments
|
||||
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
|
||||
Executable
+5
@@ -0,0 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
exec dart "$REPO_ROOT/scripts/version.dart" tag "$@" --project-root "$REPO_ROOT"
|
||||
@@ -0,0 +1,71 @@
|
||||
|
||||
$script:SmPlayerProjectRoot = Split-Path -Parent $PSScriptRoot
|
||||
$script:SmPlayerVersionCli = Join-Path $PSScriptRoot 'version.dart'
|
||||
|
||||
function Get-SmPlayerCurrentVersion {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)][string]$Platform,
|
||||
[string]$ProjectRoot = $script:SmPlayerProjectRoot
|
||||
)
|
||||
|
||||
return (& dart $script:SmPlayerVersionCli current $Platform --project-root $ProjectRoot)
|
||||
}
|
||||
|
||||
function ConvertTo-SmPlayerBuildNumber {
|
||||
param([Parameter(Mandatory = $true)][string]$Version)
|
||||
|
||||
return [int](& dart $script:SmPlayerVersionCli build-number $Version)
|
||||
}
|
||||
|
||||
function Test-SmPlayerFlutterOptionPresent {
|
||||
param(
|
||||
[string[]]$Arguments = @(),
|
||||
[Parameter(Mandatory = $true)][string]$OptionName
|
||||
)
|
||||
|
||||
foreach ($argument in @($Arguments)) {
|
||||
if ($argument -eq $OptionName -or $argument.StartsWith("$OptionName=")) { return $true }
|
||||
}
|
||||
return $false
|
||||
}
|
||||
|
||||
function Test-SmPlayerDartDefinePresent {
|
||||
param(
|
||||
[string[]]$Arguments = @(),
|
||||
[Parameter(Mandatory = $true)][string]$DefineName
|
||||
)
|
||||
|
||||
$previousArgument = ''
|
||||
foreach ($argument in @($Arguments)) {
|
||||
if ($argument -like "--dart-define=$DefineName=*") { return $true }
|
||||
if ($previousArgument -eq '--dart-define' -and $argument -like "$DefineName=*") { return $true }
|
||||
$previousArgument = $argument
|
||||
}
|
||||
return $false
|
||||
}
|
||||
|
||||
function Add-SmPlayerVersionDartDefine {
|
||||
param(
|
||||
[string[]]$Arguments = @(),
|
||||
[Parameter(Mandatory = $true)][string]$Version
|
||||
)
|
||||
|
||||
$result = @($Arguments)
|
||||
if (-not (Test-SmPlayerDartDefinePresent -Arguments $result -DefineName 'SMPLAYER_VERSION')) {
|
||||
$result += "--dart-define=SMPLAYER_VERSION=$Version"
|
||||
}
|
||||
return $result
|
||||
}
|
||||
|
||||
function Add-SmPlayerBuildName {
|
||||
param(
|
||||
[string[]]$Arguments = @(),
|
||||
[Parameter(Mandatory = $true)][string]$Version
|
||||
)
|
||||
|
||||
$result = @($Arguments)
|
||||
if (-not (Test-SmPlayerFlutterOptionPresent -Arguments $result -OptionName '--build-name')) {
|
||||
$result += @('--build-name', $Version)
|
||||
}
|
||||
return $result
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
SMPLAYER_VERSION_SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
SMPLAYER_PROJECT_ROOT="$(cd "$SMPLAYER_VERSION_SCRIPT_DIR/.." && pwd)"
|
||||
|
||||
_smplayer_version() {
|
||||
dart "$SMPLAYER_VERSION_SCRIPT_DIR/version.dart" "$@" \
|
||||
--project-root "$SMPLAYER_PROJECT_ROOT"
|
||||
}
|
||||
|
||||
smplayer_current_version() {
|
||||
_smplayer_version current "$1"
|
||||
}
|
||||
|
||||
smplayer_build_number() {
|
||||
_smplayer_version build-number "$1"
|
||||
}
|
||||
|
||||
smplayer_has_flutter_option() {
|
||||
local option_name="$1"
|
||||
shift
|
||||
for argument in "$@"; do
|
||||
[[ "$argument" == "$option_name" || "$argument" == "$option_name="* ]] && return 0
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
smplayer_has_dart_define() {
|
||||
local define_name="$1"
|
||||
shift
|
||||
local previous_argument=''
|
||||
for argument in "$@"; do
|
||||
[[ "$argument" == "--dart-define=${define_name}="* ]] && return 0
|
||||
[[ "$previous_argument" == '--dart-define' && "$argument" == "${define_name}="* ]] && return 0
|
||||
previous_argument="$argument"
|
||||
done
|
||||
return 1
|
||||
}
|
||||
@@ -0,0 +1,302 @@
|
||||
import 'dart:io';
|
||||
|
||||
const _fallbackVersion = '0.0.1';
|
||||
final _versionPattern = RegExp(r'^(\d+)\.(\d+)\.(\d+)(?:\+[0-9A-Za-z.-]+)?$');
|
||||
|
||||
typedef _Release = ({
|
||||
String platform,
|
||||
String prefix,
|
||||
String source,
|
||||
_Version version,
|
||||
});
|
||||
|
||||
final class _Version implements Comparable<_Version> {
|
||||
const _Version(this.major, this.minor, this.patch);
|
||||
|
||||
factory _Version.parse(String value) {
|
||||
final match = _versionPattern.firstMatch(value);
|
||||
if (match == null) {
|
||||
throw FormatException(
|
||||
"Unsupported version '$value'. Expected MAJOR.MINOR.PATCH.",
|
||||
);
|
||||
}
|
||||
return _Version(
|
||||
int.parse(match[1]!),
|
||||
int.parse(match[2]!),
|
||||
int.parse(match[3]!),
|
||||
);
|
||||
}
|
||||
|
||||
final int major;
|
||||
final int minor;
|
||||
final int patch;
|
||||
|
||||
_Version get nextPatch => _Version(major, minor, patch + 1);
|
||||
int get buildNumber => major * 1000000 + minor * 1000 + patch;
|
||||
|
||||
@override
|
||||
int compareTo(_Version other) {
|
||||
final majorComparison = major.compareTo(other.major);
|
||||
if (majorComparison != 0) return majorComparison;
|
||||
final minorComparison = minor.compareTo(other.minor);
|
||||
if (minorComparison != 0) return minorComparison;
|
||||
return patch.compareTo(other.patch);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() => '$major.$minor.$patch';
|
||||
}
|
||||
|
||||
Future<void> main(List<String> arguments) async {
|
||||
try {
|
||||
await _run(arguments);
|
||||
} on FormatException catch (error) {
|
||||
stderr.writeln('error: ${error.message}');
|
||||
stderr.writeln(_usage);
|
||||
exitCode = 2;
|
||||
} on StateError catch (error) {
|
||||
stderr.writeln('error: ${error.message}');
|
||||
exitCode = 1;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _run(List<String> arguments) async {
|
||||
if (arguments.isEmpty || arguments.contains('--help')) {
|
||||
stdout.writeln(_usage);
|
||||
if (arguments.isEmpty) exitCode = 2;
|
||||
return;
|
||||
}
|
||||
|
||||
final command = arguments.first;
|
||||
final options = _parseOptions(arguments.skip(1).toList());
|
||||
final projectRoot = options.projectRoot ?? _defaultProjectRoot;
|
||||
|
||||
switch (command) {
|
||||
case 'current':
|
||||
_expectOnePlatform(options);
|
||||
stdout.writeln(
|
||||
await _currentVersion(options.positional.single, projectRoot),
|
||||
);
|
||||
case 'next':
|
||||
_expectOnePlatform(options);
|
||||
final release = await _nextRelease(
|
||||
options.positional.single,
|
||||
projectRoot,
|
||||
);
|
||||
stdout.writeln(
|
||||
'${release.platform} ${release.version} ${release.source}',
|
||||
);
|
||||
case 'build-number':
|
||||
if (options.positional.length != 1 || options.hasTagOptions) {
|
||||
throw const FormatException('build-number requires one version.');
|
||||
}
|
||||
stdout.writeln(_Version.parse(options.positional.single).buildNumber);
|
||||
case 'tag':
|
||||
if (options.positional.length != 1) {
|
||||
throw const FormatException('tag requires one platform.');
|
||||
}
|
||||
await _tag(options.positional.single, projectRoot, options);
|
||||
default:
|
||||
throw FormatException("Unknown command '$command'.");
|
||||
}
|
||||
}
|
||||
|
||||
void _expectOnePlatform(_Options options) {
|
||||
if (options.positional.length != 1 || options.hasTagOptions) {
|
||||
throw const FormatException('current/next requires one platform.');
|
||||
}
|
||||
}
|
||||
|
||||
({String platform, String prefix}) _resolvePlatform(String value) {
|
||||
return switch (value.trim().toLowerCase()) {
|
||||
'windows' ||
|
||||
'pc' ||
|
||||
'macos' ||
|
||||
'darwin' => (platform: 'windows', prefix: 'v'),
|
||||
'android' => (platform: 'android', prefix: 'android-v'),
|
||||
_ => throw FormatException(
|
||||
"Unsupported platform '$value'. Expected windows, pc, macos, or android.",
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
Future<_Version> _currentVersion(String platform, String projectRoot) async {
|
||||
final pubspec = _pubspecVersion(projectRoot);
|
||||
final latest = await _latestTag(platform, projectRoot);
|
||||
return latest == null || latest.version.compareTo(pubspec) < 0
|
||||
? pubspec
|
||||
: latest.version;
|
||||
}
|
||||
|
||||
Future<_Release> _nextRelease(String platform, String projectRoot) async {
|
||||
final resolved = _resolvePlatform(platform);
|
||||
final pubspec = _pubspecVersion(projectRoot);
|
||||
final latest = await _latestTag(resolved.platform, projectRoot);
|
||||
if (latest == null || latest.version.compareTo(pubspec) < 0) {
|
||||
return (
|
||||
platform: resolved.platform,
|
||||
prefix: resolved.prefix,
|
||||
source: 'pubspec:$pubspec',
|
||||
version: pubspec,
|
||||
);
|
||||
}
|
||||
return (
|
||||
platform: resolved.platform,
|
||||
prefix: resolved.prefix,
|
||||
source: latest.tag,
|
||||
version: latest.version.nextPatch,
|
||||
);
|
||||
}
|
||||
|
||||
_Version _pubspecVersion(String projectRoot) {
|
||||
final pubspec = File('$projectRoot${Platform.pathSeparator}pubspec.yaml');
|
||||
if (!pubspec.existsSync()) return _Version.parse(_fallbackVersion);
|
||||
final match = RegExp(
|
||||
r'^version:\s*(\d+\.\d+\.\d+(?:\+[0-9A-Za-z.-]+)?)\s*$',
|
||||
multiLine: true,
|
||||
).firstMatch(pubspec.readAsStringSync());
|
||||
return _Version.parse(match?[1] ?? _fallbackVersion);
|
||||
}
|
||||
|
||||
Future<({String tag, _Version version})?> _latestTag(
|
||||
String platform,
|
||||
String projectRoot,
|
||||
) async {
|
||||
final resolved = _resolvePlatform(platform);
|
||||
final result = await Process.run('git', [
|
||||
'tag',
|
||||
'--list',
|
||||
'${resolved.prefix}*',
|
||||
], workingDirectory: projectRoot);
|
||||
if (result.exitCode != 0) return null;
|
||||
|
||||
final pattern = RegExp(
|
||||
'^${RegExp.escape(resolved.prefix)}'
|
||||
r'(\d+\.\d+\.\d+(?:\+[0-9A-Za-z.-]+)?)$',
|
||||
);
|
||||
({String tag, _Version version})? latest;
|
||||
for (final tag in '${result.stdout}'.split(RegExp(r'\r?\n'))) {
|
||||
final match = pattern.firstMatch(tag.trim());
|
||||
if (match == null) continue;
|
||||
final candidate = (tag: tag.trim(), version: _Version.parse(match[1]!));
|
||||
if (latest == null || candidate.version.compareTo(latest.version) > 0) {
|
||||
latest = candidate;
|
||||
}
|
||||
}
|
||||
return latest;
|
||||
}
|
||||
|
||||
Future<void> _tag(
|
||||
String requestedPlatform,
|
||||
String projectRoot,
|
||||
_Options options,
|
||||
) async {
|
||||
final platforms = requestedPlatform.toLowerCase() == 'both'
|
||||
? const ['windows', 'android']
|
||||
: [_resolvePlatform(requestedPlatform).platform];
|
||||
|
||||
for (final platform in platforms) {
|
||||
final release = await _nextRelease(platform, projectRoot);
|
||||
final tag = '${release.prefix}${release.version}';
|
||||
final existing = await _git(['tag', '--list', tag], projectRoot);
|
||||
if ('${existing.stdout}'.trim().isNotEmpty) {
|
||||
throw StateError("Tag '$tag' already exists. Aborting.");
|
||||
}
|
||||
|
||||
stdout.writeln('[${release.platform}] ${release.source} -> $tag');
|
||||
if (options.dryRun) {
|
||||
stdout.writeln(' DryRun: skip create/push.');
|
||||
continue;
|
||||
}
|
||||
|
||||
await _git(['tag', tag], projectRoot);
|
||||
if (options.noPush) {
|
||||
stdout.writeln(' Created local tag (--no-push).');
|
||||
continue;
|
||||
}
|
||||
await _git(['push', options.remote, tag], projectRoot);
|
||||
stdout.writeln(' Pushed to ${options.remote}.');
|
||||
}
|
||||
}
|
||||
|
||||
Future<ProcessResult> _git(List<String> arguments, String projectRoot) async {
|
||||
final result = await Process.run(
|
||||
'git',
|
||||
arguments,
|
||||
workingDirectory: projectRoot,
|
||||
);
|
||||
if (result.exitCode != 0) {
|
||||
throw StateError(
|
||||
'${result.stderr}'.trim().isEmpty
|
||||
? 'git ${arguments.join(' ')} failed with exit ${result.exitCode}.'
|
||||
: '${result.stderr}'.trim(),
|
||||
);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
_Options _parseOptions(List<String> arguments) {
|
||||
final positional = <String>[];
|
||||
String? projectRoot;
|
||||
var dryRun = false;
|
||||
var noPush = false;
|
||||
var remote = 'origin';
|
||||
|
||||
for (var index = 0; index < arguments.length; index++) {
|
||||
final argument = arguments[index];
|
||||
switch (argument) {
|
||||
case '--dry-run':
|
||||
dryRun = true;
|
||||
case '--no-push':
|
||||
noPush = true;
|
||||
case '--project-root' || '--remote':
|
||||
if (++index >= arguments.length) {
|
||||
throw FormatException('$argument requires a value.');
|
||||
}
|
||||
if (argument == '--project-root') {
|
||||
projectRoot = arguments[index];
|
||||
} else {
|
||||
remote = arguments[index];
|
||||
}
|
||||
default:
|
||||
if (argument.startsWith('--project-root=')) {
|
||||
projectRoot = argument.substring('--project-root='.length);
|
||||
} else if (argument.startsWith('--remote=')) {
|
||||
remote = argument.substring('--remote='.length);
|
||||
} else if (argument.startsWith('-')) {
|
||||
throw FormatException("Unknown option '$argument'.");
|
||||
} else {
|
||||
positional.add(argument);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (remote.isEmpty) throw const FormatException('remote cannot be empty.');
|
||||
return _Options(positional, projectRoot, dryRun, noPush, remote);
|
||||
}
|
||||
|
||||
final class _Options {
|
||||
const _Options(
|
||||
this.positional,
|
||||
this.projectRoot,
|
||||
this.dryRun,
|
||||
this.noPush,
|
||||
this.remote,
|
||||
);
|
||||
|
||||
final List<String> positional;
|
||||
final String? projectRoot;
|
||||
final bool dryRun;
|
||||
final bool noPush;
|
||||
final String remote;
|
||||
|
||||
bool get hasTagOptions => dryRun || noPush || remote != 'origin';
|
||||
}
|
||||
|
||||
String get _defaultProjectRoot =>
|
||||
File.fromUri(Platform.script).parent.parent.path;
|
||||
|
||||
const _usage = '''Usage:
|
||||
dart scripts/version.dart current <pc|windows|macos|android>
|
||||
dart scripts/version.dart next <pc|windows|macos|android>
|
||||
dart scripts/version.dart build-number <MAJOR.MINOR.PATCH>
|
||||
dart scripts/version.dart tag <pc|windows|android|both> [--dry-run] [--no-push] [--remote=<name>]''';
|
||||
Reference in New Issue
Block a user