|
| 1 | +const fs = require('fs'); |
| 2 | +const { execFileSync } = require('child_process'); |
| 3 | +const https = require('https'); |
| 4 | +const path = require('path'); |
| 5 | +const crypto = require('crypto'); |
| 6 | +const os = require('os'); |
| 7 | + |
| 8 | +/** |
| 9 | + * Dependencies for the project. |
| 10 | + * |
| 11 | + * Note: The sha256 checksums are for the files hosted at the Anaconda repository. |
| 12 | + * Note: win32 support is coming soon. |
| 13 | + */ |
| 14 | +const dependencies = { |
| 15 | + darwin: { |
| 16 | + conda: { |
| 17 | + name: 'Miniconda3-py312_24.11.1-0-MacOSX-x86_64.sh', |
| 18 | + repo: 'https://repo.anaconda.com/miniconda/', |
| 19 | + sha256: |
| 20 | + '71419eaf7f0bc016c41e8e27815609e76f2d6bcfc39426c19ca5e5cf7a2ea36f', |
| 21 | + }, |
| 22 | + }, |
| 23 | + win32: { |
| 24 | + conda: { |
| 25 | + name: 'Miniconda3-py312_24.11.1-0-Windows-x86_64.exe', |
| 26 | + repo: 'https://repo.anaconda.com/miniconda/', |
| 27 | + sha256: |
| 28 | + 'be382fc02742c734fd3c399c5e5d322bc9b43292117827ab9aa6f7446351e45c', |
| 29 | + }, |
| 30 | + }, |
| 31 | +}; |
| 32 | + |
| 33 | +/** |
| 34 | + * Download a file from a URL to a destination. |
| 35 | + * |
| 36 | + * @param {*} url The URL to download from |
| 37 | + * @param {*} dest The destination file path |
| 38 | + * @returns |
| 39 | + */ |
| 40 | +async function downloadUrlToFile(url, dest) { |
| 41 | + return new Promise((resolve, reject) => { |
| 42 | + const file = fs.createWriteStream(dest); |
| 43 | + https |
| 44 | + .get(url, (response) => { |
| 45 | + response.pipe(file); |
| 46 | + file.on('finish', () => { |
| 47 | + file.close(resolve); |
| 48 | + }); |
| 49 | + }) |
| 50 | + .on('error', (err) => { |
| 51 | + fs.unlink(dest, () => reject(err)); |
| 52 | + }); |
| 53 | + }); |
| 54 | +} |
| 55 | + |
| 56 | +/** |
| 57 | + * Check if the SHA256 hash of a file matches the expected hash. |
| 58 | + * @param {*} filePath path to the file |
| 59 | + * @param {*} expectedHash expected SHA256 hash |
| 60 | + * @returns |
| 61 | + */ |
| 62 | +async function checkFileSha256(filePath, expectedHash) { |
| 63 | + return new Promise((resolve, reject) => { |
| 64 | + const hash = crypto.createHash('sha256'); |
| 65 | + const stream = fs.createReadStream(filePath); |
| 66 | + |
| 67 | + stream.on('data', (data) => { |
| 68 | + hash.update(data); |
| 69 | + }); |
| 70 | + |
| 71 | + stream.on('end', () => { |
| 72 | + const calculatedHash = hash.digest('hex'); |
| 73 | + if (calculatedHash === expectedHash) { |
| 74 | + resolve(true); |
| 75 | + } else { |
| 76 | + resolve(false); |
| 77 | + } |
| 78 | + }); |
| 79 | + |
| 80 | + stream.on('error', (err) => { |
| 81 | + reject(err); |
| 82 | + }); |
| 83 | + }); |
| 84 | +} |
| 85 | + |
| 86 | +/** |
| 87 | + * Install Miniconda on MacOS. |
| 88 | + */ |
| 89 | +async function installCondaMacOS() { |
| 90 | + const platform = process.platform; |
| 91 | + const filename = dependencies[platform].conda.name; |
| 92 | + const url = dependencies[platform].conda.repo + filename; |
| 93 | + |
| 94 | + let condaCommand = 'conda'; |
| 95 | + // Check if the file already exists in the current directory |
| 96 | + if (fs.existsSync(filename)) { |
| 97 | + console.log('Miniconda installer already exists'); |
| 98 | + } else { |
| 99 | + console.log('Downloading Miniconda installer...'); |
| 100 | + await downloadUrlToFile(url, filename); |
| 101 | + |
| 102 | + console.log('Checking Miniconda installer checksum...'); |
| 103 | + const isValid = await checkFileSha256( |
| 104 | + filename, |
| 105 | + dependencies[platform].conda.sha256, |
| 106 | + ); |
| 107 | + if (!isValid) { |
| 108 | + throw new Error('Miniconda installer checksum does not match'); |
| 109 | + } |
| 110 | + console.log('Miniconda installer downloaded and verified'); |
| 111 | + } |
| 112 | + |
| 113 | + console.log('Installing Miniconda...'); |
| 114 | + |
| 115 | + // Installation target is in the project vendor directory, which might not exist |
| 116 | + const vendorDir = path.join(__dirname, '..', 'vendor'); |
| 117 | + if (!fs.existsSync(vendorDir)) { |
| 118 | + fs.mkdirSync(vendorDir); |
| 119 | + } |
| 120 | + |
| 121 | + execFileSync('sh', [ |
| 122 | + filename, |
| 123 | + '-b', |
| 124 | + '-p', |
| 125 | + path.join(vendorDir, 'miniconda3'), |
| 126 | + ]); |
| 127 | + |
| 128 | + // Removing the installer |
| 129 | + console.log('Removing Miniconda installer...'); |
| 130 | + fs.unlinkSync(filename); |
| 131 | + |
| 132 | + // Set the condaCommand to the path of the installed conda executable |
| 133 | + condaCommand = path.join(vendorDir, 'miniconda3', 'bin', 'conda'); |
| 134 | + |
| 135 | + return condaCommand; |
| 136 | +} |
| 137 | + |
| 138 | +/** |
| 139 | + * Install Clingo using conda. |
| 140 | + * @param {*} condaCommand path to the conda executable |
| 141 | + */ |
| 142 | +function installClingo(condaCommand) { |
| 143 | + // Use conda to install clingo in cyberismo environment |
| 144 | + execFileSync(condaCommand, [ |
| 145 | + 'install', |
| 146 | + '-n', |
| 147 | + 'cyberismo', |
| 148 | + '-c', |
| 149 | + 'conda-forge', |
| 150 | + 'clingo', |
| 151 | + '-y', |
| 152 | + ]); |
| 153 | + console.log('Clingo installed'); |
| 154 | +} |
| 155 | + |
| 156 | +/** |
| 157 | + * Check if an executable is available in the PATH. |
| 158 | + * @param {*} executable name of the executable |
| 159 | + * @returns true if the executable is available, false otherwise |
| 160 | + */ |
| 161 | +function isExecutableAvailable(executable) { |
| 162 | + try { |
| 163 | + const command = |
| 164 | + process.platform === 'win32' |
| 165 | + ? `where ${executable}` |
| 166 | + : `which ${executable}`; |
| 167 | + execSync(command); |
| 168 | + return true; |
| 169 | + } catch (error) { |
| 170 | + return false; |
| 171 | + } |
| 172 | +} |
| 173 | + |
| 174 | +/** |
| 175 | + * Check if a conda package is installed in the cyberismo environment. |
| 176 | + * @param {*} condaCommand path to the conda executable |
| 177 | + * @param {*} package name of the package |
| 178 | + * @returns true if the package is installed, false otherwise |
| 179 | + */ |
| 180 | +function isCondaPackageInstalled(condaCommand, package) { |
| 181 | + try { |
| 182 | + const output = execFileSync(condaCommand, [ |
| 183 | + 'list', |
| 184 | + '-n', |
| 185 | + 'cyberismo', |
| 186 | + package, |
| 187 | + ]).toString(); |
| 188 | + return output.includes(package); |
| 189 | + } catch (error) { |
| 190 | + return false; |
| 191 | + } |
| 192 | +} |
| 193 | + |
| 194 | +/** |
| 195 | + * Create the Cyberismo conda environment if it does not exist. |
| 196 | + * @param {*} condaCommand path to the conda executable |
| 197 | + */ |
| 198 | +function createCondaEnvironment(condaCommand) { |
| 199 | + console.log('Checking if Cyberismo conda environment exists...'); |
| 200 | + |
| 201 | + if ( |
| 202 | + execFileSync(condaCommand, ['env', 'list'], { stdio: 'pipe' }) |
| 203 | + .toString() |
| 204 | + .match(/\ncyberismo/) |
| 205 | + ) { |
| 206 | + console.log('Cyberismo conda environment already exists'); |
| 207 | + return; |
| 208 | + } |
| 209 | + |
| 210 | + console.log('Cyberismo conda environment not found. Creating...'); |
| 211 | + execFileSync(condaCommand, ['env', 'create', '-f', 'environment.yml', '-y']); |
| 212 | + console.log('Cyberismo conda environment created'); |
| 213 | +} |
| 214 | + |
| 215 | +/** |
| 216 | + * Main function to install dependencies. |
| 217 | + */ |
| 218 | +(async () => { |
| 219 | + let condaCommand = 'conda'; |
| 220 | + |
| 221 | + if (process.platform == 'darwin') { |
| 222 | + console.log('Checking if Miniconda is installed...'); |
| 223 | + if (fs.existsSync(path.join(__dirname, '..', 'vendor', 'miniconda3'))) { |
| 224 | + console.log('Miniconda already installed.'); |
| 225 | + condaCommand = path.join( |
| 226 | + __dirname, |
| 227 | + '..', |
| 228 | + 'vendor', |
| 229 | + 'miniconda3', |
| 230 | + 'bin', |
| 231 | + 'conda', |
| 232 | + ); |
| 233 | + } else { |
| 234 | + console.log('Miniconda not installed. Installing Miniconda...'); |
| 235 | + condaCommand = await installCondaMacOS(); |
| 236 | + console.log('Miniconda installation complete.'); |
| 237 | + } |
| 238 | + |
| 239 | + console.log('Conda is installed at ' + condaCommand); |
| 240 | + |
| 241 | + createCondaEnvironment(condaCommand); |
| 242 | + |
| 243 | + console.log('Checking if Clingo is installed...'); |
| 244 | + if (isCondaPackageInstalled(condaCommand, 'clingo')) { |
| 245 | + console.log('Clingo already installed'); |
| 246 | + } else { |
| 247 | + console.log('Clingo not installed. Installing Clingo...'); |
| 248 | + installClingo(condaCommand); |
| 249 | + } |
| 250 | + |
| 251 | + console.log('Dependencies installed successfully.'); |
| 252 | + console.log( |
| 253 | + '--------------------------------------------------------------------------------', |
| 254 | + ); |
| 255 | + console.log('Ensure that you have the following in your shell profile:'); |
| 256 | + console.log( |
| 257 | + 'export PATH=$PATH:"' + |
| 258 | + path.join(__dirname, '..', 'vendor', 'miniconda3', 'bin') + |
| 259 | + '"', |
| 260 | + ); |
| 261 | + console.log(); |
| 262 | + console.log('For example: '); |
| 263 | + console.log( |
| 264 | + 'echo \'export PATH=$PATH:"' + |
| 265 | + path.join(__dirname, '..', 'vendor', 'miniconda3', 'bin') + |
| 266 | + '"\' >> ~/.zprofile', |
| 267 | + ); |
| 268 | + console.log(); |
| 269 | + console.log('Without starting a new shell, you can set the path with'); |
| 270 | + console.log('source ~/.zprofile'); |
| 271 | + console.log(); |
| 272 | + console.log( |
| 273 | + 'When using Cyberismo, remember to activate the environment with', |
| 274 | + ); |
| 275 | + console.log('source activate cyberismo'); |
| 276 | + console.log( |
| 277 | + '--------------------------------------------------------------------------------', |
| 278 | + ); |
| 279 | + } else { |
| 280 | + console.error('ERROR: Unsupported platform: ' + process.platform); |
| 281 | + process.exit(1); |
| 282 | + } |
| 283 | +})(); |
0 commit comments