Skip to content

Commit 7f08f3c

Browse files
authored
Implement utility script scripts/installDevPackages.js (#473)
Implement utility script scripts/installDevPackages.js This script currently works on macOS, providing a quick and easy way to install the Clingo dependency via Miniconda. Also, the Conda environment is renamed from test-env to cyberismo.
1 parent 2e8e895 commit 7f08f3c

6 files changed

+290
-5
lines changed

.github/workflows/build-and-test-windows.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ jobs:
3030
with:
3131
python-version: 3.12
3232
auto-update-conda: true
33-
activate-environment: test-env
33+
activate-environment: cyberismo
3434
environment-file: environment.yml
3535
- name: Install pnpm
3636
uses: pnpm/action-setup@v4

.github/workflows/build-and-test.yml

+2-2
Original file line numberDiff line numberDiff line change
@@ -25,15 +25,15 @@ jobs:
2525

2626
defaults:
2727
run:
28-
shell: bash -el {0}
28+
shell: bash -el {0}
2929
steps:
3030
- uses: actions/checkout@v4
3131
- name: Setup Miniconda
3232
uses: conda-incubator/setup-miniconda@v3
3333
with:
3434
python-version: 3.12
3535
auto-update-conda: true
36-
activate-environment: test-env
36+
activate-environment: cyberismo
3737
environment-file: environment.yml
3838
- name: Install pnpm
3939
uses: pnpm/action-setup@v4

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
**/.pnp
66
.pnp.js
77
.yarn/install-state.gz
8+
vendor/
89

910
# testing
1011
**/coverage

environment.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
name: test-env
1+
name: cyberismo
22
channels:
33
- conda-forge
44
- potassco

package.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
"prettier-fix": "prettier --write .",
4242
"script:schemas": "node scripts/generateSchemaImports && prettier --write tools/data-handler/src/utils/schemas.ts",
4343
"dev": "concurrently \"pnpm --filter app dev\" \"pnpm --filter data-handler watch\"",
44-
"build-docker": "docker build -t cyberismo ."
44+
"build-docker": "docker build -t cyberismo .",
45+
"install-dev-packages": "node scripts/install-dev-packages.js"
4546
}
4647
}

scripts/install-dev-packages.js

+283
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,283 @@
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

Comments
 (0)