diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0557f8b..39952b9 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -88,23 +88,23 @@ jobs: run: | if [[ "${{ matrix.platform }}" == "ubuntu-latest" ]]; then if [[ "${{ matrix.arch }}" == "x64" ]]; then - mkdir -p ./bin/linux/x64 - cp target/x86_64-unknown-linux-gnu/release/todoctor ./bin/linux/x64/todoctor - chmod +x ./bin/linux/x64/todoctor + mkdir -p ./packages/linux-x64 + cp target/x86_64-unknown-linux-gnu/release/todoctor ./packages/linux-x64/todoctor + chmod +x ./packages/linux-x64/todoctor elif [[ "${{ matrix.arch }}" == "arm64" ]]; then - mkdir -p ./bin/linux/arm64 - cp target/aarch64-unknown-linux-gnu/release/todoctor ./bin/linux/arm64/todoctor - chmod +x ./bin/linux/arm64/todoctor + mkdir -p ./packages/linux-arm64 + cp target/aarch64-unknown-linux-gnu/release/todoctor ./packages/linux-arm64/todoctor + chmod +x ./packages/linux-arm64/todoctor fi elif [[ "${{ matrix.platform }}" == "macos-latest" ]]; then if [[ "${{ matrix.arch }}" == "x64" ]]; then - mkdir -p ./bin/macos/x64 - cp target/x86_64-apple-darwin/release/todoctor ./bin/macos/x64/todoctor - chmod +x ./bin/macos/x64/todoctor + mkdir -p ./packages/darwin-x64 + cp target/x86_64-apple-darwin/release/todoctor ./packages/darwin-x64/todoctor + chmod +x ./packages/darwin-x64/todoctor elif [[ "${{ matrix.arch }}" == "arm64" ]]; then - mkdir -p ./bin/macos/arm64 - cp target/aarch64-apple-darwin/release/todoctor ./bin/macos/arm64/todoctor - chmod +x ./bin/macos/arm64/todoctor + mkdir -p ./packages/darwin-arm64 + cp target/aarch64-apple-darwin/release/todoctor ./packages/darwin-arm64/todoctor + chmod +x ./packages/darwin-arm64/todoctor fi fi shell: bash @@ -112,8 +112,8 @@ jobs: - name: Move Binaries to Bin Folder (Windows) if: runner.os == 'Windows' run: | - mkdir bin\windows\x64 - copy target\x86_64-pc-windows-msvc\release\todoctor.exe bin\windows\x64\todoctor.exe + mkdir packages\win32-x64 + copy target\x86_64-pc-windows-msvc\release\todoctor.exe packages\win32-x64\todoctor.exe shell: cmd - name: Upload Binaries @@ -142,6 +142,9 @@ jobs: - name: Build Static Assets run: pnpm run build:preview + - name: Build Package Structure + run: node ./scripts/create-packages.js + - name: Download Binaries for Linux x64 uses: actions/download-artifact@v4 with: @@ -174,17 +177,17 @@ jobs: - name: Set Execute Permissions on Binaries run: | - chmod +x ./bin/linux/x64/todoctor - chmod +x ./bin/linux/arm64/todoctor - chmod +x ./bin/macos/x64/todoctor - chmod +x ./bin/macos/arm64/todoctor + chmod +x ./packages/linux-x64/todoctor + chmod +x ./packages/linux-arm64/todoctor + chmod +x ./packages/darwin-x64/todoctor + chmod +x ./packages/darwin-arm64/todoctor - name: Verify Binary Permissions run: | - ls -l ./bin/linux/x64/todoctor - ls -l ./bin/linux/arm64/todoctor - ls -l ./bin/macos/x64/todoctor - ls -l ./bin/macos/arm64/todoctor + ls -l ./packages/linux-x64/todoctor + ls -l ./packages/linux-arm64/todoctor + ls -l ./packages/darwin-x64/todoctor + ls -l ./packages/darwin-arm64/todoctor - name: Create GitHub Release run: pnpm run ci:changelog @@ -199,3 +202,16 @@ jobs: - name: Publish to NPM run: npm publish --access public --no-git-checks --provenance + + - name: Publish Packages to NPM + run: | + for pkg in packages/*; do + if [ -d "$pkg" ]; then + echo "Publishing package $pkg" + cd "$pkg" + npm publish --access public --no-git-checks --provenance + cd - + fi + done + env: + NODE_AUTH_TOKEN: ${{ secrets.NODE_AUTH_TOKEN }} diff --git a/.gitignore b/.gitignore index 1f7d82a..762bb23 100644 --- a/.gitignore +++ b/.gitignore @@ -6,7 +6,7 @@ node_modules/ target/ # Build -!bin/todoctor.js +packages/ dist/ bin/ diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..1fdad1f --- /dev/null +++ b/.npmrc @@ -0,0 +1,3 @@ +link-workspace-packages = true +prefer-workspace-packages = true +recursive-install = true diff --git a/bin/todoctor.js b/bin/todoctor.js index 9518d27..ac52210 100755 --- a/bin/todoctor.js +++ b/bin/todoctor.js @@ -1,42 +1,37 @@ #!/usr/bin/env node +import { createRequire } from 'node:module' import { spawn } from 'node:child_process' import { fileURLToPath } from 'node:url' import path from 'node:path' import os from 'node:os' -import fs from 'node:fs' +let require = createRequire(import.meta.url) let filename = fileURLToPath(import.meta.url) let dirname = path.dirname(filename) let platform = os.platform() let arch = os.arch() -export let binaries = { - 'win32:x64': 'windows/x64/todoctor.exe', - 'darwin:arm64': 'macos/arm64/todoctor', - 'linux:arm64': 'linux/arm64/todoctor', - 'darwin:x64': 'macos/x64/todoctor', - 'linux:x64': 'linux/x64/todoctor', -} - -let key = `${platform}:${arch}` -let relativePath = binaries[key] - -if (!relativePath) { - console.error(`Unsupported platform or architecture: ${platform}, ${arch}`) - process.exit(1) -} - -let binaryPath = path.join(dirname, relativePath) - -if (!fs.existsSync(binaryPath)) { - console.error(`Binary not found: ${binaryPath}`) +let packageName = `@todoctor/${platform}-${arch}` +let binaryPath + +try { + let packageBinaryFile = 'todoctor' + if (platform === 'win32') { + packageBinaryFile += '.exe' + } + binaryPath = require.resolve(`${packageName}/${packageBinaryFile}`) +} catch (error) { + console.error(`Failed to find binary for ${packageName}`) + console.error(error) process.exit(1) } +let distPath = path.join(dirname, '../dist') let args = process.argv.slice(2) -let child = spawn(binaryPath, args, { stdio: 'inherit' }) +let env = { ...process.env, DIST_PATH: distPath } +let child = spawn(binaryPath, args, { stdio: 'inherit', env }) child.on('exit', code => { process.exit(code) diff --git a/package.json b/package.json index 99282b5..2f725de 100644 --- a/package.json +++ b/package.json @@ -23,12 +23,19 @@ "start": "pnpm run /^start:/", "start:preview": "vite", "build": "pnpm run /^build:/", - "build:lib": "cargo build --release && node ./scripts/build.js", + "build:lib": "cargo build --release && node \"./scripts/create-packages.js\" && node \"./scripts/build.js\"", "build:preview": "vite build", "docs:build": "DOCS=true vite build", - "release": "pnpm release:check && pnpm release:version", + "release": "pnpm release:check && pnpm release:version && pnpm release:git", "release:check": "pnpm test && pnpm run build", - "release:version": "changelogen --output changelog.md --release --push", + "release:git": "pnpm release:git:add && pnpm release:git:commit && pnpm release:git:tag && pnpm release:git:push", + "release:git:add": "git add .", + "release:git:commit": "git commit -m \"build: publish v$(node -p \"require('./package.json').version\")\"", + "release:git:tag": "git tag v$(node -p \"require('./package.json').version\")", + "release:git:push": "git push --follow-tags", + "release:version": "pnpm release:version:root && pnpm release:version:packages", + "release:version:root": "changelogen --output changelog.md --release --no-commit --no-tag", + "release:version:packages": "node ./scripts/version.js && pnpm install", "ci:changelog": "changelogithub", "ci:clear": "clear-package-json package.json --output package.json", "test": "pnpm run /^test:/", @@ -59,6 +66,13 @@ "./build", "./dist" ], + "optionalDependencies": { + "@todoctor/darwin-arm64": "^1.2.0", + "@todoctor/darwin-x64": "^1.2.0", + "@todoctor/linux-arm64": "^1.2.0", + "@todoctor/linux-x64": "^1.2.0", + "@todoctor/win32-x64": "^1.2.0" + }, "devDependencies": { "@azat-io/eslint-config-typescript": "^1.10.0", "@azat-io/stylelint-config": "^0.1.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 00115c2..6d14506 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -7,6 +7,22 @@ settings: importers: .: + optionalDependencies: + '@todoctor/darwin-arm64': + specifier: ^1.2.0 + version: link:packages/darwin-arm64 + '@todoctor/darwin-x64': + specifier: ^1.2.0 + version: link:packages/darwin-x64 + '@todoctor/linux-arm64': + specifier: ^1.2.0 + version: link:packages/linux-arm64 + '@todoctor/linux-x64': + specifier: ^1.2.0 + version: link:packages/linux-x64 + '@todoctor/win32-x64': + specifier: ^1.2.0 + version: link:packages/win32-x64 devDependencies: '@azat-io/eslint-config-typescript': specifier: ^1.10.0 @@ -168,6 +184,16 @@ importers: specifier: ^2.0.2 version: 2.0.2(rollup@4.24.0)(vite@5.4.10(@types/node@22.7.9)(lightningcss@1.27.0)(terser@5.36.0)) + packages/darwin-arm64: {} + + packages/darwin-x64: {} + + packages/linux-arm64: {} + + packages/linux-x64: {} + + packages/win32-x64: {} + packages: '@ampproject/remapping@2.3.0': diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml new file mode 100644 index 0000000..f10aa33 --- /dev/null +++ b/pnpm-workspace.yaml @@ -0,0 +1,2 @@ +packages: + - packages/** diff --git a/scripts/build.js b/scripts/build.js index 574df2a..a340294 100644 --- a/scripts/build.js +++ b/scripts/build.js @@ -1,37 +1,33 @@ -import { fileURLToPath } from 'node:url' import path from 'node:path' import fs from 'node:fs' import os from 'node:os' +import { platforms } from './platforms.js' + let platform = os.platform() let arch = os.arch() -let platformMap = { - win32: 'windows', - darwin: 'macos', - linux: 'linux', -} - -let archMap = { - arm64: 'arm64', - x64: 'x64', -} +let userPlatform = platforms.find(p => p.name === platform) -let filename = fileURLToPath(import.meta.url) -let dirname = path.dirname(filename) - -let platformName = platformMap[platform] -let archName = archMap[arch] - -if (!platformName || !archName) { +if (!userPlatform?.arch.includes(arch)) { console.error(`Unsupported platform or architecture: ${platform}, ${arch}`) process.exit(1) } -let binaryName = platform === 'win32' ? 'todoctor.exe' : 'todoctor' - -let sourcePath = path.join(dirname, '..', 'target', 'release', binaryName) -let destDir = path.join(dirname, '../bin/', platformName, archName) +let binaryName = ['todoctor', userPlatform.extension].filter(Boolean).join('.') + +let sourcePath = path.join( + import.meta.dirname, + '..', + 'target', + 'release', + binaryName, +) +let destDir = path.join( + import.meta.dirname, + '../packages/', + `${platform}-${arch}`, +) let destPath = path.join(destDir, binaryName) if (!fs.existsSync(destDir)) { diff --git a/scripts/create-packages.js b/scripts/create-packages.js new file mode 100644 index 0000000..b678585 --- /dev/null +++ b/scripts/create-packages.js @@ -0,0 +1,73 @@ +/* eslint-disable perfectionist/sort-objects */ + +import fs from 'node:fs/promises' +import path from 'node:path' + +import { getPackageJson } from './get-package-json.js' +import { platforms } from './platforms.js' + +let packagesDir = path.join(import.meta.dirname, '../packages') +let rootPackageJson = await getPackageJson() + +for (let platform of platforms) { + for (let arch of platform.arch) { + let packageDir = path.join(packagesDir, `${platform.name}-${arch}`) + + await fs.mkdir(packageDir, { + recursive: true, + }) + + let name = `@todoctor/${platform.name}-${arch}` + + fs.writeFile( + path.join(packageDir, 'package.json'), + JSON.stringify( + { + name, + description: `Platform-specific binary for Todoctor (${platform.name}, ${arch})`, + version: rootPackageJson.version, + repository: rootPackageJson.repository, + author: rootPackageJson.author, + license: rootPackageJson.license, + keywords: rootPackageJson.keywords, + os: [platform.name], + cpu: [arch], + }, + null, + 2, + ), + ) + + fs.writeFile( + path.join(packageDir, 'readme.md'), + [ + `# Todoctor (${platform.name[0].toUpperCase() + platform.name.slice(1)}, ${arch.toUpperCase()})`, + '', + '', + '', + `[![Version](https://img.shields.io/npm/v/${name}.svg?color=2c7f50&labelColor=353c3c)](https://npmjs.com/package/${name})`, + '[![GitHub License](https://img.shields.io/badge/license-MIT-232428.svg?color=2c7f50&labelColor=353c3c)](https://github.com/azat-io/todoctor/blob/main/license)', + '', + 'This is a platform-specific binary package for Todoctor. It contains the pre-compiled binary executable for your operating system and architecture.', + '', + '**Note:** This package is not meant to be installed directly by users. Instead, please install the main Todoctor package, which will automatically download the correct binary for your platform.', + '', + '## Usage', + '', + '```sh', + 'npx todoctor', + '```', + '', + '## License', + '', + 'MIT © [Azat S.](https://azat.io)', + ].join('\n'), + ) + } +} diff --git a/scripts/get-package-json.js b/scripts/get-package-json.js new file mode 100644 index 0000000..e4273e7 --- /dev/null +++ b/scripts/get-package-json.js @@ -0,0 +1,7 @@ +import fs from 'node:fs/promises' +import path from 'node:path' + +export let getPackageJson = async () => { + let rootPackageJsonPath = path.join(import.meta.dirname, '../package.json') + return JSON.parse(await fs.readFile(rootPackageJsonPath, 'utf-8')) +} diff --git a/scripts/platforms.js b/scripts/platforms.js new file mode 100644 index 0000000..1ccb0fd --- /dev/null +++ b/scripts/platforms.js @@ -0,0 +1,17 @@ +export let platforms = [ + { + arch: ['arm64', 'x64'], + extension: null, + name: 'darwin', + }, + { + arch: ['arm64', 'x64'], + extension: null, + name: 'linux', + }, + { + extension: 'exe', + arch: ['x64'], + name: 'win32', + }, +] diff --git a/scripts/version.js b/scripts/version.js new file mode 100644 index 0000000..f8f1c29 --- /dev/null +++ b/scripts/version.js @@ -0,0 +1,20 @@ +import fs from 'node:fs/promises' +import path from 'node:path' + +import { getPackageJson } from './get-package-json.js' + +let rootPackageJson = await getPackageJson() + +let { version } = rootPackageJson + +rootPackageJson.optionalDependencies = Object.fromEntries( + Object.keys(rootPackageJson.optionalDependencies).map(key => [ + key, + `^${version}`, + ]), +) + +await fs.writeFile( + path.join(import.meta.dirname, '../package.json'), + JSON.stringify(rootPackageJson, null, 2), +) diff --git a/src/fs/get_dist_path.rs b/src/fs/get_dist_path.rs index ead79a4..5848839 100644 --- a/src/fs/get_dist_path.rs +++ b/src/fs/get_dist_path.rs @@ -1,29 +1,8 @@ -use std::{env, fs, path::PathBuf}; +use std::{env, path::PathBuf}; pub fn get_dist_path() -> Option { - let exe_path = match env::current_exe() { - Ok(path) => path, - Err(e) => { - eprintln!("Error getting current exe path: {:?}", e); - return None; - } - }; - let real_exe_path = fs::canonicalize(&exe_path).unwrap_or(exe_path); - - fn ascend_path(mut path: PathBuf, levels: usize) -> Option { - for _ in 0..levels { - path = path.parent()?.to_path_buf(); - } - Some(path) - } - - let levels_up = 4; - - let project_root = ascend_path(real_exe_path, levels_up)?; - let dist_path = project_root.join("dist"); - if dist_path.exists() { - return Some(dist_path); - } - - None + env::var("DIST_PATH") + .ok() + .map(PathBuf::from) + .filter(|p| p.exists()) }