diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..03a7920 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,40 @@ +name: Build + +on: + push: + branches: + - main + pull_request: + branches: + - '*' + +jobs: + build: + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, windows-latest, macos-latest] + runs-on: ${{ matrix.os }} + name: "Build - ${{ matrix.os }}" + + steps: + - uses: actions/checkout@v4 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.10' + + - name: Install dependencies + run: npm install + + - name: Pretest + run: npm run pretest + + - name: Test + run: npm test diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..d02d6a9 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,44 @@ +name: Release + +on: + push: + tags: + - 'v*' + +jobs: + release: + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, windows-latest, macos-latest] + runs-on: ${{ matrix.os }} + name: "Release - ${{ matrix.os }}" + + steps: + - uses: actions/checkout@v4 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.10' + + - name: Install dependencies + run: npm install + + - name: Build + run: npm run build + + - name: Create release + uses: softprops/action-gh-release@v2 + with: + files: dist/* + tag_name: ${{ github.ref }} + name: ${{ github.ref }} + body: | + This is a release for version ${{ github.ref }}. + It contains the compiled files from the build process. diff --git a/README.md b/README.md index 4a39dd2..b8c56bb 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,21 @@ This package enables Node.js applications to use a CAN bus over USB. -## Making a release +## Compiling + +### Prerequisites + +- Supported version of NodeJS (preferably a LTS version) +- A C/C++ compiler that supports C++20 +- Internet connection to pull from upstream [CANBridge](https://github.com/unofficial-rev-port/CANBridge) + +### Steps + +1. Clone repository +2. Run `npm i` to install dependencies and compile the Node APIs +3. To test, run `npm run pretest` to prepare the testing and `npm test` to actually perform the testing of the repository + +## Making a release (for repository owners) 1. Check out the `main` branch 2. Update `version` field in `package.json` @@ -10,6 +24,6 @@ This package enables Node.js applications to use a CAN bus over USB. 4. Commit change to git 5. Run `git tag v` 6. Run `git push` -7. Run `git push --tags` +7. Run `git push --tags` 8. Run `npm publish --access public` 9. Create a new release on GitHub with an explanation of the changes diff --git a/binding.gyp b/binding.gyp index 919189e..819c101 100644 --- a/binding.gyp +++ b/binding.gyp @@ -5,7 +5,7 @@ 'sources': [ 'src/addon.cc', 'src/canWrapper.cc', - ], + ], 'include_dirs': [ "src/", "externalCompileTimeDeps/include", @@ -15,21 +15,55 @@ "NAPI_VERSION=<(napi_build_version)" ], 'dependencies': [" Promise; registerDeviceToHAL: (descriptor:string, messageId:Number, messageMask:number) => number; @@ -67,34 +58,30 @@ export class CanBridge { ackHeartbeats: () => void; constructor() { - try { - const addon = require('node-gyp-build')(path.join(__dirname, '..')); - - this.getDevices = promisify(addon.getDevices); - this.registerDeviceToHAL = addon.registerDeviceToHAL; - this.unregisterDeviceFromHAL = promisify(addon.unregisterDeviceFromHAL); - this.receiveMessage = addon.receiveMessage; - this.openStreamSession = addon.openStreamSession; - this.readStreamSession = addon.readStreamSession; - this.closeStreamSession = addon.closeStreamSession; - this.getCANDetailStatus = addon.getCANDetailStatus; - this.sendCANMessage = addon.sendCANMessage; - this.sendHALMessage = addon.sendHALMessage; - this.initializeNotifier = addon.initializeNotifier; - this.waitForNotifierAlarm = promisify(addon.waitForNotifierAlarm); - this.stopNotifier = addon.stopNotifier; - this.writeDfuToBin = promisify(addon.writeDfuToBin); - this.openHALStreamSession = addon.openHALStreamSession; - this.readHALStreamSession = addon.readHALStreamSession; - this.closeHALStreamSession = addon.closeHALStreamSession; - this.setThreadPriority = addon.setThreadPriority; - this.setSparkMaxHeartbeatData = addon.setSparkMaxHeartbeatData; - this.startRevCommonHeartbeat = addon.startRevCommonHeartbeat; - this.ackHeartbeats = addon.ackHeartbeats; - this.stopHeartbeats = addon.stopHeartbeats; - } catch (e: any) { - throw new CanBridgeInitializationError(e); - } + const addon = require('node-gyp-build')(path.join(__dirname, '..')); + + this.getDevices = promisify(addon.getDevices); + this.registerDeviceToHAL = addon.registerDeviceToHAL; + this.unregisterDeviceFromHAL = promisify(addon.unregisterDeviceFromHAL); + this.receiveMessage = addon.receiveMessage; + this.openStreamSession = addon.openStreamSession; + this.readStreamSession = addon.readStreamSession; + this.closeStreamSession = addon.closeStreamSession; + this.getCANDetailStatus = addon.getCANDetailStatus; + this.sendCANMessage = addon.sendCANMessage; + this.sendHALMessage = addon.sendHALMessage; + this.initializeNotifier = addon.initializeNotifier; + this.waitForNotifierAlarm = promisify(addon.waitForNotifierAlarm); + this.stopNotifier = addon.stopNotifier; + this.writeDfuToBin = promisify(addon.writeDfuToBin); + this.openHALStreamSession = addon.openHALStreamSession; + this.readHALStreamSession = addon.readHALStreamSession; + this.closeHALStreamSession = addon.closeHALStreamSession; + this.setThreadPriority = addon.setThreadPriority; + this.setSparkMaxHeartbeatData = addon.setSparkMaxHeartbeatData; + this.startRevCommonHeartbeat = addon.startRevCommonHeartbeat; + this.ackHeartbeats = addon.ackHeartbeats; + this.stopHeartbeats = addon.stopHeartbeats; } } diff --git a/scripts/download-CanBridge.mjs b/scripts/download-CanBridge.mjs index 265caab..591e02c 100644 --- a/scripts/download-CanBridge.mjs +++ b/scripts/download-CanBridge.mjs @@ -2,47 +2,127 @@ import * as fs from "fs"; import * as path from "path"; import axios from 'axios'; import AdmZip from 'adm-zip'; +import { platform, arch } from 'os'; -const canBridgeTag = "v2.3.0"; -const canBridgeReleaseAssetUrlPrefix = `https://github.com/REVrobotics/CANBridge/releases/download/${canBridgeTag}`; +const canBridgeTag = "v2.3.6"; +const canBridgeReleaseAssetUrlPrefix = `https://github.com/unofficial-rev-port/CANBridge/releases/download/${canBridgeTag}`; const externalCompileTimeDepsPath = 'externalCompileTimeDeps'; -const runtimeArtifactsPath = path.join('prebuilds', 'win32-x64'); +const runtimeArtifactsPath = { + win: 'prebuilds/win32-x64', + osx: 'prebuilds/darwin-osxuniversal', + linux: 'prebuilds/linux-x64', + linuxArm: 'prebuilds/linux-arm64', + linuxArm32: 'prebuilds/linux-arm32' +}; const tempDir = 'temp'; try { - await Promise.all(Array.of( - downloadCanBridgeArtifact('CANBridge.lib', externalCompileTimeDepsPath), - downloadCanBridgeArtifact('CANBridge.dll', runtimeArtifactsPath), - downloadCanBridgeArtifact('wpiHal.lib', externalCompileTimeDepsPath), - downloadCanBridgeArtifact('wpiHal.dll', runtimeArtifactsPath), - downloadCanBridgeArtifact('wpiutil.lib', externalCompileTimeDepsPath), - downloadCanBridgeArtifact('wpiutil.dll', runtimeArtifactsPath), - downloadCanBridgeArtifact('headers.zip', tempDir), - )); - + // TODO: Do not hardcode the filenames, instead get them from the GitHub API -> Look at Octokit: https://github.com/octokit/octokit.js + await Promise.all([ + 'CANBridge-linuxarm32.zip', + 'CANBridge-linuxarm64.zip', + 'CANBridge-linuxx86-64-Linux64.zip', + 'CANBridge-osxuniversal-macOS.zip', + 'CANBridge-windowsx86-64-Win64.zip', + 'headers.zip' + ].map(filename => downloadCanBridgeArtifact(filename))); console.log("CANBridge download completed"); - console.log("Extracting headers"); + + console.log("Extracting headers"); + const zipFiles = fs.readdirSync(tempDir).filter(filename => filename.endsWith('.zip') && filename !== 'headers.zip'); + for (const filename of zipFiles) { + await unzipCanBridgeArtifact(filename, tempDir); + } const headersZip = new AdmZip(path.join(tempDir, "headers.zip")); + headersZip.extractAllTo(path.join(externalCompileTimeDepsPath, 'include')); + console.log("Headers extracted"); + + moveRuntimeDeps(); - await headersZip.extractAllTo(path.join(externalCompileTimeDepsPath, 'include')); + moveCompileTimeDeps(); } catch (e) { if (axios.isAxiosError(e) && e.request) { - console.error(`Failed to download CANBridge file ${e.request.protocol}//${e.request.host}/${e.request.path}`); + console.error(`Failed to download CANBridge file ${e.request.protocol}//${e.request.host}${e.request.path}`); } else { - console.error(`Failed to download CANBridge`); + console.error(`Other error occurred: ${e.message}`); // For non-axios errors, the stacktrace will likely be helpful throw e; } process.exit(1); } finally { - if (fs.existsSync(tempDir)) { - fs.rmSync(tempDir, { recursive: true }); + //if (fs.existsSync(tempDir)) { + // fs.rmSync(tempDir, { recursive: true, force: true}); + //} +} + +/** + * Move external compile time dependencies to the correct directory + * + * This function is used to move the external compile time dependencies to the correct directory based on the platform and architecture from downloaded artifacts + */ +function moveCompileTimeDeps() { + console.log("Moving external compile time dependencies to correct directories"); + if (!fs.existsSync(externalCompileTimeDepsPath)) { + fs.mkdirSync(externalCompileTimeDepsPath, { recursive: true }); + } + if (platform() === 'win32') { + const deps = ['CANBridge.lib', 'wpiHal.lib', 'wpiutil.lib']; + deps.forEach(dep => moveExternalCompileTimeDeps(path.join('win32-x64', dep))); + } else if (platform() === 'darwin') { + const deps = ['libCANBridge.a']; + deps.forEach(dep => moveExternalCompileTimeDeps(path.join('darwin-osxuniversal', dep))); + } else if (platform() === 'linux') { + const deps = ['libCANBridge.a']; + const archDepMap = { + x64: 'linux-x64', + arm64: 'linux-arm64', + arm: 'linux-arm32' + }; + deps.forEach(dep => moveExternalCompileTimeDeps(path.join(archDepMap[arch()], dep))); + } + console.log("External compile time dependencies moved to correct directories"); +} + +/** + * Move runtime dependencies to the correct directory + * + * This function is used to move the runtime dependencies to the correct directory based on the platform and architecture from downloaded artifacts + */ +function moveRuntimeDeps() { + console.log("Moving artifacts to correct directories"); + if (!fs.existsSync('prebuilds')) { + fs.mkdirSync('prebuilds', { recursive: true }); + } + if (platform() === 'win32') { + const deps = ['CANBridge.dll', 'wpiHal.dll', 'wpiutil.dll']; + deps.forEach(dep => moveRuntimeArtifactsDeps(path.join('win32-x64', dep), runtimeArtifactsPath.win)); + } else if (platform() === 'darwin') { + const deps = ['libCANBridge.dylib', 'libwpiHal.dylib', 'libwpiutil.dylib']; + deps.forEach(dep => moveRuntimeArtifactsDeps(path.join('darwin-osxuniversal', dep), runtimeArtifactsPath.osx)); + } else if (platform() === 'linux') { + const deps = ['libCANBridge.so', 'libwpiHal.so', 'libwpiutil.so']; + if (arch() === 'x64') { + deps.forEach(dep => moveRuntimeArtifactsDeps(path.join('linux-x64', dep), runtimeArtifactsPath.linux)); + } + if (arch() === 'arm64') { + deps.forEach(dep => moveRuntimeArtifactsDeps(path.join('linux-arm64', dep), runtimeArtifactsPath.linuxArm)); + } + if (arch() === 'arm') { + deps.forEach(dep => moveRuntimeArtifactsDeps(path.join('linux-arm32', dep), runtimeArtifactsPath.linuxArm32)); + } } + console.log("CANBridge artifacts moved to correct directories"); } -async function downloadCanBridgeArtifact(filename, destDir) { +/** + * Download artifacts from the CANBridge GitHub release page + * + * @param {*} filename filename of the artifact to download + * @param {*} destDir destination directory to save the artifact, defaults to tempDir + */ +async function downloadCanBridgeArtifact(filename, destDir = tempDir) { fs.mkdirSync(destDir, { recursive: true }); const response = await axios.get(`${canBridgeReleaseAssetUrlPrefix}/${filename}`, { responseType: "stream" }); const fileStream = fs.createWriteStream(`${destDir}/${filename}`); @@ -51,3 +131,40 @@ async function downloadCanBridgeArtifact(filename, destDir) { fileStream.on('finish', resolve); }); } + +/** + * Unzip the CANBridge artifacts + * + * @param {string} filename - filename of the artifact to unzip + * @param {string} destDir - destination directory to unzip the artifact + */ +async function unzipCanBridgeArtifact(filename, destDir) { + const zip = new AdmZip(`${destDir}/${filename}`); + let filepath; + if (filename.includes('linuxarm32')) filepath = "linux-arm32"; + else if (filename.includes('linuxarm64')) filepath = "linux-arm64"; + else if (filename.includes('linuxx86-64')) filepath = "linux-x64"; + else if (filename.includes('osxuniversal')) filepath = "darwin-osxuniversal"; + else if (filename.includes('windowsx86-64')) filepath = "win32-x64"; + zip.extractAllTo(`${destDir}/${filepath}`); +} + +/** + * Move runtime artifacts to the correct directory + * + * @param {*} filename filename of the artifact to move + * @param {*} destDir destination directory to save the artifact + */ +function moveRuntimeArtifactsDeps(filename, destDir) { + fs.mkdirSync(destDir, { recursive: true }); + fs.renameSync(path.join(tempDir, filename), path.join(destDir, path.basename(filename))); +} + +/** + * Move External Compile Time Dependencies to the correct directory + * + * @param {*} filename filename of the artifact to move + */ +function moveExternalCompileTimeDeps(filename) { + fs.renameSync(path.join(tempDir, filename), path.join(externalCompileTimeDepsPath, path.basename(filename))); +} \ No newline at end of file diff --git a/src/canWrapper.cc b/src/canWrapper.cc index bd71daf..c7ca114 100644 --- a/src/canWrapper.cc +++ b/src/canWrapper.cc @@ -816,4 +816,4 @@ void stopHeartbeats(const Napi::CallbackInfo& info) { // Send disabled SPARK and REV common heartbeats and un-schedule them for the future _sendCANMessage(descriptor, SPARK_HEARTBEAT_ID, disabledSparkHeartbeat, SPARK_HEARTBEAT_LENGTH, repeatPeriod); _sendCANMessage(descriptor, REV_COMMON_HEARTBEAT_ID, disabledRevCommonHeartbeat, REV_COMMON_HEARTBEAT_LENGTH, repeatPeriod); -} +} \ No newline at end of file