diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..51c2ecb --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,41 @@ +name: Release EXE + +on: + push: + tags: ['v*'] + workflow_dispatch: + +jobs: + build: + runs-on: windows-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: 18 + cache: npm + + - name: Install deps + run: npm ci + + - name: Build EXE + run: npx pkg bm.js --target node18-win-x64 --output dist/bm.exe + + - name: Patch icon + run: node patch_icon3.js + + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + name: bm-downloader-windows + path: dist/bm.exe + + - name: Create GitHub Release + if: startsWith(github.ref, 'refs/tags/') + uses: softprops/action-gh-release@v2 + with: + files: dist/bm.exe + name: BurmaldaMusic CLI ${{ github.ref_name }} + generate_release_notes: true + prerelease: false diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..d67691f --- /dev/null +++ b/package-lock.json @@ -0,0 +1,237 @@ +{ + "name": "bm-downloader", + "version": "0.1.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "bm-downloader", + "version": "0.1.1", + "license": "MIT", + "bin": { + "bm": "bm.js" + }, + "devDependencies": { + "png-to-ico": "^3.0.2", + "rcedit": "^4.0.1" + } + }, + "node_modules/@malept/cross-spawn-promise": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@malept/cross-spawn-promise/-/cross-spawn-promise-1.1.1.tgz", + "integrity": "sha512-RTBGWL5FWQcg9orDOCcp4LvItNzUPcyEU9bwaeJX0rJ1IQxzucC48Y0/sQLp/g6t99IQgAlGIaesJS+gTn7tVQ==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/malept" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/subscription/pkg/npm-.malept-cross-spawn-promise?utm_medium=referral&utm_source=npm_fund" + } + ], + "license": "Apache-2.0", + "dependencies": { + "cross-spawn": "^7.0.1" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/@types/node": { + "version": "25.9.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.5.tgz", + "integrity": "sha512-OScDchr2fwuUmWdf4kZ9h7PcJiYDVInhJizG/biAq3cAvqwYktuy/TYGGdZNMtNTFUP7rnb0NU4TUdm82kt4Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": ">=7.24.0 <7.24.7" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/cross-spawn-windows-exe": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/cross-spawn-windows-exe/-/cross-spawn-windows-exe-1.2.0.tgz", + "integrity": "sha512-mkLtJJcYbDCxEG7Js6eUnUNndWjyUZwJ3H7bErmmtOYU/Zb99DyUkpamuIZE0b3bhmJyZ7D90uS6f+CGxRRjOw==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/malept" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/subscription/pkg/npm-cross-spawn-windows-exe?utm_medium=referral&utm_source=npm_fund" + } + ], + "license": "Apache-2.0", + "dependencies": { + "@malept/cross-spawn-promise": "^1.1.0", + "is-wsl": "^2.2.0", + "which": "^2.0.2" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "dev": true, + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/png-to-ico": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/png-to-ico/-/png-to-ico-3.0.2.tgz", + "integrity": "sha512-36vvp3/YF7LPYkUEOj08/WgB1wI63qW391YfOhOWckgOtGkarr9+EhPBimcCmRP7fJaG4EaXQhTTaQ8qqdj8aA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "^25.5.0", + "minimist": "^1.2.8", + "pngjs": "^7.0.0" + }, + "bin": { + "png-to-ico": "bin/cli.js" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/pngjs": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-7.0.0.tgz", + "integrity": "sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.19.0" + } + }, + "node_modules/rcedit": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/rcedit/-/rcedit-4.0.1.tgz", + "integrity": "sha512-bZdaQi34krFWhrDn+O53ccBDw0MkAT2Vhu75SqhtvhQu4OPyFM4RoVheyYiVQYdjhUi6EJMVWQ0tR6bCIYVkUg==", + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn-windows-exe": "^1.1.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/undici-types": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz", + "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==", + "dev": true, + "license": "MIT" + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + } + } +} diff --git a/package.json b/package.json index 6eebcc3..a798866 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "assets": [] }, "devDependencies": { - "png-to-ico": "^3.0.2" + "png-to-ico": "^3.0.2", + "rcedit": "^4.0.1" } } diff --git a/patch_icon3.js b/patch_icon3.js new file mode 100644 index 0000000..88d84bd --- /dev/null +++ b/patch_icon3.js @@ -0,0 +1,110 @@ +'use strict' +const { spawnSync } = require('child_process') +const fs = require('fs') +const os = require('os') +const path = require('path') + +const EXE = path.resolve(__dirname, 'dist', 'bm.exe') +const CLEAN = fs.existsSync(path.resolve(__dirname, 'dist', 'bm_clean.exe')) + ? path.resolve(__dirname, 'dist', 'bm_clean.exe') + : EXE +const ICON = path.resolve(__dirname, 'icon.ico') + +function findRcedit() { + const candidates = [ + path.join(os.homedir(), 'AppData', 'Roaming', 'npm', 'node_modules', 'rcedit', 'bin', 'rcedit-x64.exe'), + ] + try { + const pkgDir = path.dirname(require.resolve('rcedit/package.json')) + candidates.unshift(path.join(pkgDir, 'bin', 'rcedit-x64.exe')) + } catch {} + const fromPath = spawnSync('where', ['rcedit-x64.exe'], { encoding: 'utf8' }) + if (fromPath.stdout) candidates.unshift(fromPath.stdout.trim().split('\n')[0]) + return candidates.find(p => { try { return fs.statSync(p).isFile() } catch { return false } }) +} + +const RCEDIT = findRcedit() +if (!RCEDIT) { console.error('rcedit not found'); process.exit(1) } +console.log('rcedit:', RCEDIT) + +if (CLEAN === EXE) { + const tmp = EXE.replace('.exe', '_clean.exe') + fs.copyFileSync(EXE, tmp) +} + +const cleanPath = CLEAN === EXE ? EXE.replace('.exe', '_clean.exe') : CLEAN +const origBuf = fs.readFileSync(cleanPath) + +// Find pkg position values +const kbuf = Buffer.from("var PAYLOAD_POSITION = '", 'ascii') +let pos = {} +for (let i = 0; i < origBuf.length - 200; i++) { + if (origBuf.slice(i, i + kbuf.length).equals(kbuf)) { + const block = origBuf.slice(i, i + 400).toString('ascii') + const pp = block.match(/PAYLOAD_POSITION = '(\d+)\s*'/) + const ps = block.match(/PAYLOAD_SIZE = '(\d+)\s*'/) + const pp2 = block.match(/PRELUDE_POSITION = '(\d+)\s*'/) + const ps2 = block.match(/PRELUDE_SIZE = '(\d+)\s*'/) + if (pp) { + pos = { + PAYLOAD_POSITION: +pp[1], + PAYLOAD_SIZE: +ps[1], + PRELUDE_POSITION: +pp2[1], + PRELUDE_SIZE: +ps2[1], + } + console.log('pkg vars:', pos) + break + } + } +} + +if (!pos.PAYLOAD_POSITION) { console.error('vars not found'); process.exit(1) } + +const cleanBuf = origBuf +const overlay = cleanBuf.slice(pos.PAYLOAD_POSITION) +const peOnly = cleanBuf.slice(0, pos.PAYLOAD_POSITION) +console.log(`overlay: ${overlay.length}b, PE: ${peOnly.length}b`) + +fs.writeFileSync(EXE, peOnly) + +console.log('rcedit...') +const r = spawnSync(RCEDIT, [EXE, '--set-icon', ICON], { stdio: 'inherit' }) +if (r.status !== 0) { process.exit(1) } + +const patchedPE = fs.readFileSync(EXE) +const newPP = patchedPE.length +const newPRP = newPP + pos.PAYLOAD_SIZE +console.log(`new PE: ${patchedPE.length} new PP: ${newPP} new PRP: ${newPRP}`) + +const full = Buffer.from(Buffer.concat([patchedPE, overlay])) + +function replaceNum(buf, oldVal, newVal) { + const oldStr = String(oldVal) + const newStr = String(newVal) + const needle = Buffer.from(`'${oldStr}`, 'ascii') + let count = 0 + let i = 0 + while ((i = buf.indexOf(needle, i)) !== -1) { + let end = i + 1 + while (buf[end] !== 0x27 && end < i + 30) end++ + const fieldLen = end - i - 1 + if (fieldLen < oldStr.length) { i++; continue } + const padded = newStr + ' '.repeat(fieldLen - newStr.length) + buf.write(padded, i + 1, 'ascii') + console.log(` patched '${oldStr}' → '${newStr}' at offset ${i}`) + count++ + i = end + } + return count +} + +let total = 0 +total += replaceNum(full, pos.PAYLOAD_POSITION, newPP) +total += replaceNum(full, pos.PRELUDE_POSITION, newPRP) +console.log(`total patches: ${total}`) + +fs.writeFileSync(EXE, full) +fs.unlinkSync(EXE + '.notail').catch?.(() => {}) +try { fs.unlinkSync(CLEAN + '.pe_only') } catch {} + +console.log(`done: ${EXE} = ${full.length} bytes`)