mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-03-20 03:16:46 -07:00
Prepare Windows release and signing process (#16)
This commit is contained in:
163
scripts/build-yomitan.mjs
Normal file
163
scripts/build-yomitan.mjs
Normal file
@@ -0,0 +1,163 @@
|
||||
import fs from 'node:fs';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import { createHash } from 'node:crypto';
|
||||
import { execFileSync } from 'node:child_process';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const repoRoot = path.resolve(dirname, '..');
|
||||
const submoduleDir = path.join(repoRoot, 'vendor', 'subminer-yomitan');
|
||||
const submodulePackagePath = path.join(submoduleDir, 'package.json');
|
||||
const submodulePackageLockPath = path.join(submoduleDir, 'package-lock.json');
|
||||
const buildOutputDir = path.join(repoRoot, 'build', 'yomitan');
|
||||
const stampPath = path.join(buildOutputDir, '.subminer-build.json');
|
||||
const zipPath = path.join(submoduleDir, 'builds', 'yomitan-chrome.zip');
|
||||
const bunCommand = process.versions.bun ? process.execPath : 'bun';
|
||||
const dependencyStampPath = path.join(submoduleDir, 'node_modules', '.subminer-package-lock-hash');
|
||||
|
||||
function run(command, args, cwd) {
|
||||
execFileSync(command, args, { cwd, stdio: 'inherit' });
|
||||
}
|
||||
|
||||
function escapePowerShellString(value) {
|
||||
return value.replaceAll("'", "''");
|
||||
}
|
||||
|
||||
function readCommand(command, args, cwd) {
|
||||
return execFileSync(command, args, { cwd, encoding: 'utf8' }).trim();
|
||||
}
|
||||
|
||||
function readStamp() {
|
||||
try {
|
||||
return JSON.parse(fs.readFileSync(stampPath, 'utf8'));
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function hashFile(filePath) {
|
||||
const hash = createHash('sha256');
|
||||
hash.update(fs.readFileSync(filePath));
|
||||
return hash.digest('hex');
|
||||
}
|
||||
|
||||
function ensureSubmodulePresent() {
|
||||
if (!fs.existsSync(submodulePackagePath)) {
|
||||
throw new Error(
|
||||
'Missing vendor/subminer-yomitan submodule. Run `git submodule update --init --recursive`.',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function getSourceState() {
|
||||
const revision = readCommand('git', ['rev-parse', 'HEAD'], submoduleDir);
|
||||
const dirty = readCommand('git', ['status', '--short', '--untracked-files=no'], submoduleDir);
|
||||
return { revision, dirty };
|
||||
}
|
||||
|
||||
function isBuildCurrent(force) {
|
||||
if (force) {
|
||||
return false;
|
||||
}
|
||||
if (!fs.existsSync(path.join(buildOutputDir, 'manifest.json'))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const stamp = readStamp();
|
||||
if (!stamp) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const currentState = getSourceState();
|
||||
return stamp.revision === currentState.revision && stamp.dirty === currentState.dirty;
|
||||
}
|
||||
|
||||
function ensureDependenciesInstalled() {
|
||||
const nodeModulesDir = path.join(submoduleDir, 'node_modules');
|
||||
const currentLockHash = hashFile(submodulePackageLockPath);
|
||||
let installedLockHash = '';
|
||||
try {
|
||||
installedLockHash = fs.readFileSync(dependencyStampPath, 'utf8').trim();
|
||||
} catch {}
|
||||
|
||||
if (!fs.existsSync(nodeModulesDir) || installedLockHash !== currentLockHash) {
|
||||
run(bunCommand, ['install', '--no-save'], submoduleDir);
|
||||
fs.mkdirSync(nodeModulesDir, { recursive: true });
|
||||
fs.writeFileSync(dependencyStampPath, `${currentLockHash}\n`, 'utf8');
|
||||
}
|
||||
}
|
||||
|
||||
function installAndBuild() {
|
||||
ensureDependenciesInstalled();
|
||||
run(bunCommand, ['./dev/bin/build.js', '--target', 'chrome'], submoduleDir);
|
||||
}
|
||||
|
||||
function extractBuild() {
|
||||
if (!fs.existsSync(zipPath)) {
|
||||
throw new Error(`Expected Yomitan build artifact at ${zipPath}`);
|
||||
}
|
||||
|
||||
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'subminer-yomitan-'));
|
||||
try {
|
||||
if (process.platform === 'win32') {
|
||||
run(
|
||||
'powershell.exe',
|
||||
[
|
||||
'-NoProfile',
|
||||
'-NonInteractive',
|
||||
'-ExecutionPolicy',
|
||||
'Bypass',
|
||||
'-Command',
|
||||
`Expand-Archive -LiteralPath '${escapePowerShellString(zipPath)}' -DestinationPath '${escapePowerShellString(tempDir)}' -Force`,
|
||||
],
|
||||
repoRoot,
|
||||
);
|
||||
} else {
|
||||
run('unzip', ['-qo', zipPath, '-d', tempDir], repoRoot);
|
||||
}
|
||||
fs.rmSync(buildOutputDir, { recursive: true, force: true });
|
||||
fs.mkdirSync(path.dirname(buildOutputDir), { recursive: true });
|
||||
fs.cpSync(tempDir, buildOutputDir, { recursive: true });
|
||||
if (!fs.existsSync(path.join(buildOutputDir, 'manifest.json'))) {
|
||||
throw new Error(`Extracted Yomitan build missing manifest.json in ${buildOutputDir}`);
|
||||
}
|
||||
} finally {
|
||||
fs.rmSync(tempDir, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
function writeStamp() {
|
||||
const state = getSourceState();
|
||||
fs.writeFileSync(
|
||||
stampPath,
|
||||
`${JSON.stringify(
|
||||
{
|
||||
revision: state.revision,
|
||||
dirty: state.dirty,
|
||||
builtAt: new Date().toISOString(),
|
||||
},
|
||||
null,
|
||||
2,
|
||||
)}\n`,
|
||||
'utf8',
|
||||
);
|
||||
}
|
||||
|
||||
function main() {
|
||||
const force = process.argv.includes('--force');
|
||||
ensureSubmodulePresent();
|
||||
|
||||
if (isBuildCurrent(force)) {
|
||||
process.stdout.write(`Yomitan build current: ${buildOutputDir}\n`);
|
||||
return;
|
||||
}
|
||||
|
||||
process.stdout.write('Building Yomitan Chrome artifact...\n');
|
||||
installAndBuild();
|
||||
extractBuild();
|
||||
writeStamp();
|
||||
process.stdout.write(`Yomitan extracted to ${buildOutputDir}\n`);
|
||||
}
|
||||
|
||||
main();
|
||||
101
scripts/configure-plugin-binary-path.mjs
Normal file
101
scripts/configure-plugin-binary-path.mjs
Normal file
@@ -0,0 +1,101 @@
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
|
||||
function normalizeCandidate(candidate) {
|
||||
if (typeof candidate !== 'string') return '';
|
||||
const trimmed = candidate.trim();
|
||||
return trimmed.length > 0 ? trimmed : '';
|
||||
}
|
||||
|
||||
function fileExists(candidate) {
|
||||
try {
|
||||
return fs.statSync(candidate).isFile();
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function unique(values) {
|
||||
return Array.from(new Set(values.filter((value) => value.length > 0)));
|
||||
}
|
||||
|
||||
function findWindowsBinary(repoRoot) {
|
||||
const homeDir = process.env.HOME?.trim() || process.env.USERPROFILE?.trim() || '';
|
||||
const appDataDir = process.env.APPDATA?.trim() || '';
|
||||
const derivedLocalAppData =
|
||||
appDataDir && /[\\/]Roaming$/i.test(appDataDir)
|
||||
? appDataDir.replace(/[\\/]Roaming$/i, `${path.sep}Local`)
|
||||
: '';
|
||||
const localAppData =
|
||||
process.env.LOCALAPPDATA?.trim() ||
|
||||
derivedLocalAppData ||
|
||||
(homeDir ? path.join(homeDir, 'AppData', 'Local') : '');
|
||||
const programFiles = process.env.ProgramFiles?.trim() || 'C:\\Program Files';
|
||||
const programFilesX86 = process.env['ProgramFiles(x86)']?.trim() || 'C:\\Program Files (x86)';
|
||||
|
||||
const candidates = unique([
|
||||
normalizeCandidate(process.env.SUBMINER_BINARY_PATH),
|
||||
normalizeCandidate(process.env.SUBMINER_APPIMAGE_PATH),
|
||||
localAppData ? path.join(localAppData, 'Programs', 'SubMiner', 'SubMiner.exe') : '',
|
||||
path.join(programFiles, 'SubMiner', 'SubMiner.exe'),
|
||||
path.join(programFilesX86, 'SubMiner', 'SubMiner.exe'),
|
||||
'C:\\SubMiner\\SubMiner.exe',
|
||||
path.join(repoRoot, 'release', 'win-unpacked', 'SubMiner.exe'),
|
||||
path.join(repoRoot, 'release', 'SubMiner', 'SubMiner.exe'),
|
||||
path.join(repoRoot, 'release', 'SubMiner.exe'),
|
||||
]);
|
||||
|
||||
return candidates.find((candidate) => fileExists(candidate)) || '';
|
||||
}
|
||||
|
||||
function rewriteBinaryPath(configPath, binaryPath) {
|
||||
const content = fs.readFileSync(configPath, 'utf8');
|
||||
const normalizedPath = binaryPath.replace(/\r?\n/g, ' ').trim();
|
||||
const updated = content.replace(/^binary_path=.*$/m, `binary_path=${normalizedPath}`);
|
||||
if (updated !== content) {
|
||||
fs.writeFileSync(configPath, updated, 'utf8');
|
||||
}
|
||||
}
|
||||
|
||||
function rewriteSocketPath(configPath, socketPath) {
|
||||
const content = fs.readFileSync(configPath, 'utf8');
|
||||
const normalizedPath = socketPath.replace(/\r?\n/g, ' ').trim();
|
||||
const updated = content.replace(/^socket_path=.*$/m, `socket_path=${normalizedPath}`);
|
||||
if (updated !== content) {
|
||||
fs.writeFileSync(configPath, updated, 'utf8');
|
||||
}
|
||||
}
|
||||
|
||||
const [, , configPathArg, repoRootArg, platformArg] = process.argv;
|
||||
const configPath = normalizeCandidate(configPathArg);
|
||||
const repoRoot = normalizeCandidate(repoRootArg) || process.cwd();
|
||||
const platform = normalizeCandidate(platformArg) || process.platform;
|
||||
|
||||
if (!configPath) {
|
||||
console.error('[ERROR] Missing plugin config path');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!fileExists(configPath)) {
|
||||
console.error(`[ERROR] Plugin config not found: ${configPath}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (platform !== 'win32') {
|
||||
console.log('[INFO] Skipping binary_path rewrite for non-Windows platform');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const windowsSocketPath = '\\\\.\\pipe\\subminer-socket';
|
||||
rewriteSocketPath(configPath, windowsSocketPath);
|
||||
|
||||
const binaryPath = findWindowsBinary(repoRoot);
|
||||
if (!binaryPath) {
|
||||
console.warn(
|
||||
`[WARN] Configured plugin socket_path=${windowsSocketPath} but could not detect SubMiner.exe; set binary_path manually or provide SUBMINER_BINARY_PATH`,
|
||||
);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
rewriteBinaryPath(configPath, binaryPath);
|
||||
console.log(`[INFO] Configured plugin socket_path=${windowsSocketPath} binary_path=${binaryPath}`);
|
||||
175
scripts/get-mpv-window-windows.ps1
Normal file
175
scripts/get-mpv-window-windows.ps1
Normal file
@@ -0,0 +1,175 @@
|
||||
param(
|
||||
[ValidateSet('geometry')]
|
||||
[string]$Mode = 'geometry',
|
||||
[string]$SocketPath
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
try {
|
||||
Add-Type -TypeDefinition @"
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
public static class SubMinerWindowsHelper {
|
||||
public delegate bool EnumWindowsProc(IntPtr hWnd, IntPtr lParam);
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public struct RECT {
|
||||
public int Left;
|
||||
public int Top;
|
||||
public int Right;
|
||||
public int Bottom;
|
||||
}
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
public static extern bool EnumWindows(EnumWindowsProc lpEnumFunc, IntPtr lParam);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
public static extern bool IsWindowVisible(IntPtr hWnd);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
public static extern bool IsIconic(IntPtr hWnd);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
public static extern IntPtr GetForegroundWindow();
|
||||
|
||||
[DllImport("user32.dll", SetLastError = true)]
|
||||
public static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint processId);
|
||||
|
||||
[DllImport("user32.dll", SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
public static extern bool GetWindowRect(IntPtr hWnd, out RECT rect);
|
||||
|
||||
[DllImport("dwmapi.dll")]
|
||||
public static extern int DwmGetWindowAttribute(IntPtr hwnd, int dwAttribute, out RECT pvAttribute, int cbAttribute);
|
||||
}
|
||||
"@
|
||||
|
||||
$DWMWA_EXTENDED_FRAME_BOUNDS = 9
|
||||
|
||||
function Get-WindowBounds {
|
||||
param([IntPtr]$hWnd)
|
||||
|
||||
$rect = New-Object SubMinerWindowsHelper+RECT
|
||||
$size = [System.Runtime.InteropServices.Marshal]::SizeOf($rect)
|
||||
$dwmResult = [SubMinerWindowsHelper]::DwmGetWindowAttribute(
|
||||
$hWnd,
|
||||
$DWMWA_EXTENDED_FRAME_BOUNDS,
|
||||
[ref]$rect,
|
||||
$size
|
||||
)
|
||||
|
||||
if ($dwmResult -ne 0) {
|
||||
if (-not [SubMinerWindowsHelper]::GetWindowRect($hWnd, [ref]$rect)) {
|
||||
return $null
|
||||
}
|
||||
}
|
||||
|
||||
$width = $rect.Right - $rect.Left
|
||||
$height = $rect.Bottom - $rect.Top
|
||||
if ($width -le 0 -or $height -le 0) {
|
||||
return $null
|
||||
}
|
||||
|
||||
return [PSCustomObject]@{
|
||||
X = $rect.Left
|
||||
Y = $rect.Top
|
||||
Width = $width
|
||||
Height = $height
|
||||
Area = $width * $height
|
||||
}
|
||||
}
|
||||
|
||||
$commandLineByPid = @{}
|
||||
if (-not [string]::IsNullOrWhiteSpace($SocketPath)) {
|
||||
foreach ($process in Get-CimInstance Win32_Process) {
|
||||
$commandLineByPid[[uint32]$process.ProcessId] = $process.CommandLine
|
||||
}
|
||||
}
|
||||
|
||||
$mpvMatches = New-Object System.Collections.Generic.List[object]
|
||||
$foregroundWindow = [SubMinerWindowsHelper]::GetForegroundWindow()
|
||||
$callback = [SubMinerWindowsHelper+EnumWindowsProc]{
|
||||
param([IntPtr]$hWnd, [IntPtr]$lParam)
|
||||
|
||||
if (-not [SubMinerWindowsHelper]::IsWindowVisible($hWnd)) {
|
||||
return $true
|
||||
}
|
||||
|
||||
if ([SubMinerWindowsHelper]::IsIconic($hWnd)) {
|
||||
return $true
|
||||
}
|
||||
|
||||
[uint32]$windowProcessId = 0
|
||||
[void][SubMinerWindowsHelper]::GetWindowThreadProcessId($hWnd, [ref]$windowProcessId)
|
||||
if ($windowProcessId -eq 0) {
|
||||
return $true
|
||||
}
|
||||
|
||||
try {
|
||||
$process = Get-Process -Id $windowProcessId -ErrorAction Stop
|
||||
} catch {
|
||||
return $true
|
||||
}
|
||||
|
||||
if ($process.ProcessName -ine 'mpv') {
|
||||
return $true
|
||||
}
|
||||
|
||||
if (-not [string]::IsNullOrWhiteSpace($SocketPath)) {
|
||||
$commandLine = $commandLineByPid[[uint32]$windowProcessId]
|
||||
if ([string]::IsNullOrWhiteSpace($commandLine)) {
|
||||
return $true
|
||||
}
|
||||
if (
|
||||
($commandLine -notlike "*--input-ipc-server=$SocketPath*") -and
|
||||
($commandLine -notlike "*--input-ipc-server $SocketPath*")
|
||||
) {
|
||||
return $true
|
||||
}
|
||||
}
|
||||
|
||||
$bounds = Get-WindowBounds -hWnd $hWnd
|
||||
if ($null -eq $bounds) {
|
||||
return $true
|
||||
}
|
||||
|
||||
$mpvMatches.Add([PSCustomObject]@{
|
||||
HWnd = $hWnd
|
||||
X = $bounds.X
|
||||
Y = $bounds.Y
|
||||
Width = $bounds.Width
|
||||
Height = $bounds.Height
|
||||
Area = $bounds.Area
|
||||
IsForeground = ($foregroundWindow -ne [IntPtr]::Zero -and $hWnd -eq $foregroundWindow)
|
||||
})
|
||||
|
||||
return $true
|
||||
}
|
||||
|
||||
[void][SubMinerWindowsHelper]::EnumWindows($callback, [IntPtr]::Zero)
|
||||
|
||||
$focusedMatch = $mpvMatches | Where-Object { $_.IsForeground } | Select-Object -First 1
|
||||
if ($null -ne $focusedMatch) {
|
||||
[Console]::Error.WriteLine('focus=focused')
|
||||
} else {
|
||||
[Console]::Error.WriteLine('focus=not-focused')
|
||||
}
|
||||
|
||||
if ($mpvMatches.Count -eq 0) {
|
||||
Write-Output 'not-found'
|
||||
exit 0
|
||||
}
|
||||
|
||||
$bestMatch = if ($null -ne $focusedMatch) {
|
||||
$focusedMatch
|
||||
} else {
|
||||
$mpvMatches | Sort-Object -Property Area, Width, Height -Descending | Select-Object -First 1
|
||||
}
|
||||
Write-Output "$($bestMatch.X),$($bestMatch.Y),$($bestMatch.Width),$($bestMatch.Height)"
|
||||
} catch {
|
||||
[Console]::Error.WriteLine($_.Exception.Message)
|
||||
exit 1
|
||||
}
|
||||
84
scripts/prepare-build-assets.mjs
Normal file
84
scripts/prepare-build-assets.mjs
Normal file
@@ -0,0 +1,84 @@
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { execFileSync } from 'node:child_process';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const scriptDir = path.dirname(fileURLToPath(import.meta.url));
|
||||
const repoRoot = path.resolve(scriptDir, '..');
|
||||
const rendererSourceDir = path.join(repoRoot, 'src', 'renderer');
|
||||
const rendererOutputDir = path.join(repoRoot, 'dist', 'renderer');
|
||||
const scriptsOutputDir = path.join(repoRoot, 'dist', 'scripts');
|
||||
const windowsHelperSourcePath = path.join(scriptDir, 'get-mpv-window-windows.ps1');
|
||||
const windowsHelperOutputPath = path.join(scriptsOutputDir, 'get-mpv-window-windows.ps1');
|
||||
const macosHelperSourcePath = path.join(scriptDir, 'get-mpv-window-macos.swift');
|
||||
const macosHelperBinaryPath = path.join(scriptsOutputDir, 'get-mpv-window-macos');
|
||||
const macosHelperSourceCopyPath = path.join(scriptsOutputDir, 'get-mpv-window-macos.swift');
|
||||
|
||||
function ensureDir(dirPath) {
|
||||
fs.mkdirSync(dirPath, { recursive: true });
|
||||
}
|
||||
|
||||
function copyFile(sourcePath, outputPath) {
|
||||
ensureDir(path.dirname(outputPath));
|
||||
fs.copyFileSync(sourcePath, outputPath);
|
||||
}
|
||||
|
||||
function copyRendererAssets() {
|
||||
copyFile(path.join(rendererSourceDir, 'index.html'), path.join(rendererOutputDir, 'index.html'));
|
||||
copyFile(path.join(rendererSourceDir, 'style.css'), path.join(rendererOutputDir, 'style.css'));
|
||||
fs.cpSync(path.join(rendererSourceDir, 'fonts'), path.join(rendererOutputDir, 'fonts'), {
|
||||
recursive: true,
|
||||
force: true,
|
||||
});
|
||||
process.stdout.write(`Staged renderer assets in ${rendererOutputDir}\n`);
|
||||
}
|
||||
|
||||
function stageWindowsHelper() {
|
||||
copyFile(windowsHelperSourcePath, windowsHelperOutputPath);
|
||||
process.stdout.write(`Staged Windows helper: ${windowsHelperOutputPath}\n`);
|
||||
}
|
||||
|
||||
function fallbackToMacosSource() {
|
||||
copyFile(macosHelperSourcePath, macosHelperSourceCopyPath);
|
||||
process.stdout.write(`Staged macOS helper source fallback: ${macosHelperSourceCopyPath}\n`);
|
||||
}
|
||||
|
||||
function shouldSkipMacosHelperBuild() {
|
||||
return process.env.SUBMINER_SKIP_MACOS_HELPER_BUILD === '1';
|
||||
}
|
||||
|
||||
function buildMacosHelper() {
|
||||
if (shouldSkipMacosHelperBuild()) {
|
||||
process.stdout.write('Skipping macOS helper build (SUBMINER_SKIP_MACOS_HELPER_BUILD=1)\n');
|
||||
fallbackToMacosSource();
|
||||
return;
|
||||
}
|
||||
|
||||
if (process.platform !== 'darwin') {
|
||||
process.stdout.write('Skipping macOS helper build (not on macOS)\n');
|
||||
fallbackToMacosSource();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
execFileSync('swiftc', ['-O', macosHelperSourcePath, '-o', macosHelperBinaryPath], {
|
||||
stdio: 'inherit',
|
||||
});
|
||||
fs.chmodSync(macosHelperBinaryPath, 0o755);
|
||||
process.stdout.write(`Built macOS helper: ${macosHelperBinaryPath}\n`);
|
||||
} catch (error) {
|
||||
process.stdout.write('Failed to compile macOS helper; using source fallback.\n');
|
||||
fallbackToMacosSource();
|
||||
if (error instanceof Error) {
|
||||
process.stderr.write(`${error.message}\n`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function main() {
|
||||
copyRendererAssets();
|
||||
stageWindowsHelper();
|
||||
buildMacosHelper();
|
||||
}
|
||||
|
||||
main();
|
||||
@@ -17,4 +17,5 @@ paths=(
|
||||
"src"
|
||||
)
|
||||
|
||||
exec bunx prettier "$@" "${paths[@]}"
|
||||
BUN_BIN="$(command -v bun.exe || command -v bun)"
|
||||
exec "$BUN_BIN" x prettier "$@" "${paths[@]}"
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { readdirSync } from 'node:fs';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { relative, resolve } from 'node:path';
|
||||
import { spawnSync } from 'node:child_process';
|
||||
|
||||
const repoRoot = resolve(new URL('..', import.meta.url).pathname);
|
||||
const repoRoot = resolve(fileURLToPath(new URL('..', import.meta.url)));
|
||||
|
||||
const lanes = {
|
||||
'bun-src-full': {
|
||||
|
||||
223
scripts/test-plugin-binary-windows.lua
Normal file
223
scripts/test-plugin-binary-windows.lua
Normal file
@@ -0,0 +1,223 @@
|
||||
local function assert_equal(actual, expected, message)
|
||||
if actual == expected then
|
||||
return
|
||||
end
|
||||
error((message or "assert_equal failed") .. "\nexpected: " .. tostring(expected) .. "\nactual: " .. tostring(actual))
|
||||
end
|
||||
|
||||
local function assert_true(condition, message)
|
||||
if condition then
|
||||
return
|
||||
end
|
||||
error(message or "assert_true failed")
|
||||
end
|
||||
|
||||
local function with_env(env, callback)
|
||||
local original_getenv = os.getenv
|
||||
os.getenv = function(name)
|
||||
local value = env[name]
|
||||
if value ~= nil then
|
||||
return value
|
||||
end
|
||||
return original_getenv(name)
|
||||
end
|
||||
|
||||
local ok, result = pcall(callback)
|
||||
os.getenv = original_getenv
|
||||
if not ok then
|
||||
error(result)
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
local function create_binary_module(config)
|
||||
local binary_module = dofile("plugin/subminer/binary.lua")
|
||||
local entries = config.entries or {}
|
||||
|
||||
local binary = binary_module.create({
|
||||
mp = config.mp,
|
||||
utils = {
|
||||
file_info = function(path)
|
||||
local entry = entries[path]
|
||||
if entry == "file" then
|
||||
return { is_dir = false }
|
||||
end
|
||||
if entry == "dir" then
|
||||
return { is_dir = true }
|
||||
end
|
||||
return nil
|
||||
end,
|
||||
join_path = function(...)
|
||||
return table.concat({ ... }, "\\")
|
||||
end,
|
||||
},
|
||||
opts = {
|
||||
binary_path = config.binary_path or "",
|
||||
},
|
||||
state = {},
|
||||
environment = {
|
||||
is_windows = function()
|
||||
return config.is_windows == true
|
||||
end,
|
||||
},
|
||||
log = {
|
||||
subminer_log = function() end,
|
||||
},
|
||||
})
|
||||
|
||||
return binary
|
||||
end
|
||||
|
||||
do
|
||||
local binary = create_binary_module({
|
||||
is_windows = true,
|
||||
binary_path = "C:\\Users\\tester\\AppData\\Local\\Programs\\SubMiner\\SubMiner",
|
||||
entries = {
|
||||
["C:\\Users\\tester\\AppData\\Local\\Programs\\SubMiner\\SubMiner.exe"] = "file",
|
||||
},
|
||||
})
|
||||
|
||||
assert_equal(
|
||||
binary.find_binary(),
|
||||
"C:\\Users\\tester\\AppData\\Local\\Programs\\SubMiner\\SubMiner.exe",
|
||||
"windows resolver should append .exe for configured binary_path"
|
||||
)
|
||||
end
|
||||
|
||||
do
|
||||
local binary = create_binary_module({
|
||||
is_windows = true,
|
||||
mp = {
|
||||
command_native = function(command)
|
||||
local args = command.args or {}
|
||||
if args[1] == "powershell.exe" then
|
||||
return {
|
||||
status = 0,
|
||||
stdout = "C:\\Users\\tester\\AppData\\Local\\Programs\\SubMiner\\SubMiner.exe\n",
|
||||
stderr = "",
|
||||
}
|
||||
end
|
||||
return {
|
||||
status = 1,
|
||||
stdout = "",
|
||||
stderr = "unexpected command",
|
||||
}
|
||||
end,
|
||||
},
|
||||
entries = {
|
||||
["C:\\Users\\tester\\AppData\\Local\\Programs\\SubMiner\\SubMiner.exe"] = "file",
|
||||
},
|
||||
})
|
||||
|
||||
assert_equal(
|
||||
binary.find_binary(),
|
||||
"C:\\Users\\tester\\AppData\\Local\\Programs\\SubMiner\\SubMiner.exe",
|
||||
"windows resolver should recover binary from running SubMiner process"
|
||||
)
|
||||
end
|
||||
|
||||
do
|
||||
local binary = create_binary_module({
|
||||
is_windows = true,
|
||||
binary_path = "C:\\Users\\tester\\AppData\\Local\\Programs\\SubMiner",
|
||||
entries = {
|
||||
["C:\\Users\\tester\\AppData\\Local\\Programs\\SubMiner"] = "dir",
|
||||
["C:\\Users\\tester\\AppData\\Local\\Programs\\SubMiner\\SubMiner.exe"] = "file",
|
||||
},
|
||||
})
|
||||
|
||||
assert_equal(
|
||||
binary.find_binary(),
|
||||
"C:\\Users\\tester\\AppData\\Local\\Programs\\SubMiner\\SubMiner.exe",
|
||||
"windows resolver should accept install directory binary_path"
|
||||
)
|
||||
end
|
||||
|
||||
do
|
||||
local resolved = with_env({
|
||||
LOCALAPPDATA = "C:\\Users\\tester\\AppData\\Local",
|
||||
HOME = "",
|
||||
USERPROFILE = "C:\\Users\\tester",
|
||||
ProgramFiles = "C:\\Program Files",
|
||||
["ProgramFiles(x86)"] = "C:\\Program Files (x86)",
|
||||
}, function()
|
||||
local binary = create_binary_module({
|
||||
is_windows = true,
|
||||
entries = {
|
||||
["C:\\Users\\tester\\AppData\\Local\\Programs\\SubMiner\\SubMiner.exe"] = "file",
|
||||
},
|
||||
})
|
||||
return binary.find_binary()
|
||||
end)
|
||||
|
||||
assert_equal(
|
||||
resolved,
|
||||
"C:\\Users\\tester\\AppData\\Local\\Programs\\SubMiner\\SubMiner.exe",
|
||||
"windows auto-detection should probe LOCALAPPDATA install path"
|
||||
)
|
||||
end
|
||||
|
||||
do
|
||||
local resolved = with_env({
|
||||
APPDATA = "C:\\Users\\tester\\AppData\\Roaming",
|
||||
LOCALAPPDATA = "",
|
||||
HOME = "",
|
||||
USERPROFILE = "C:\\Users\\tester",
|
||||
ProgramFiles = "C:\\Program Files",
|
||||
["ProgramFiles(x86)"] = "C:\\Program Files (x86)",
|
||||
}, function()
|
||||
local binary = create_binary_module({
|
||||
is_windows = true,
|
||||
entries = {
|
||||
["C:\\Users\\tester\\AppData\\Local\\Programs\\SubMiner\\SubMiner.exe"] = "file",
|
||||
},
|
||||
})
|
||||
return binary.find_binary()
|
||||
end)
|
||||
|
||||
assert_equal(
|
||||
resolved,
|
||||
"C:\\Users\\tester\\AppData\\Local\\Programs\\SubMiner\\SubMiner.exe",
|
||||
"windows auto-detection should derive Local install path from APPDATA"
|
||||
)
|
||||
end
|
||||
|
||||
do
|
||||
local resolved = with_env({
|
||||
SUBMINER_BINARY_PATH = "C:\\Portable\\SubMiner\\SubMiner",
|
||||
}, function()
|
||||
local binary = create_binary_module({
|
||||
is_windows = true,
|
||||
entries = {
|
||||
["C:\\Portable\\SubMiner\\SubMiner.exe"] = "file",
|
||||
},
|
||||
})
|
||||
return binary.find_binary()
|
||||
end)
|
||||
|
||||
assert_equal(
|
||||
resolved,
|
||||
"C:\\Portable\\SubMiner\\SubMiner.exe",
|
||||
"windows env override should resolve .exe suffix"
|
||||
)
|
||||
end
|
||||
|
||||
do
|
||||
local binary = create_binary_module({
|
||||
is_windows = true,
|
||||
binary_path = "C:\\Users\\tester\\AppData\\Local\\Programs\\SubMiner",
|
||||
entries = {
|
||||
["C:\\Users\\tester\\AppData\\Local\\Programs\\SubMiner"] = "dir",
|
||||
["C:\\Users\\tester\\AppData\\Local\\Programs\\SubMiner\\SubMiner.exe"] = "file",
|
||||
},
|
||||
})
|
||||
|
||||
assert_true(binary.ensure_binary_available() == true, "ensure_binary_available should cache discovered windows binary")
|
||||
assert_equal(
|
||||
binary.find_binary(),
|
||||
"C:\\Users\\tester\\AppData\\Local\\Programs\\SubMiner\\SubMiner.exe",
|
||||
"ensure_binary_available should not break follow-up lookup"
|
||||
)
|
||||
end
|
||||
|
||||
print("plugin windows binary resolver tests: OK")
|
||||
@@ -802,4 +802,29 @@ do
|
||||
)
|
||||
end
|
||||
|
||||
do
|
||||
local recorded, err = run_plugin_scenario({
|
||||
platform = "windows",
|
||||
process_list = "",
|
||||
option_overrides = {
|
||||
binary_path = "C:/Users/test/AppData/Local/Programs/SubMiner/SubMiner.exe",
|
||||
auto_start = "yes",
|
||||
auto_start_visible_overlay = "yes",
|
||||
socket_path = "/tmp/subminer-socket",
|
||||
},
|
||||
input_ipc_server = "\\\\.\\pipe\\subminer-socket",
|
||||
media_title = "Random Movie",
|
||||
files = {
|
||||
["C:/Users/test/AppData/Local/Programs/SubMiner/SubMiner.exe"] = true,
|
||||
},
|
||||
})
|
||||
assert_true(recorded ~= nil, "plugin failed to load for Windows legacy socket config scenario: " .. tostring(err))
|
||||
fire_event(recorded, "file-loaded")
|
||||
local start_call = find_start_call(recorded.async_calls)
|
||||
assert_true(
|
||||
start_call ~= nil,
|
||||
"Windows plugin should normalize legacy /tmp socket_path values to the named pipe default"
|
||||
)
|
||||
end
|
||||
|
||||
print("plugin start gate regression tests: OK")
|
||||
|
||||
Reference in New Issue
Block a user