Skip to content

Commit c2f605f

Browse files
committed
Expose a "glean" command
This exposes a "glean" command to be used as an interface for running the glean_parser by glean.js consuming products. It takes care of setting up a virtual environment and making sure that the parser and its dependencies are up to date.
1 parent a498264 commit c2f605f

File tree

4 files changed

+229
-0
lines changed

4 files changed

+229
-0
lines changed

glean/package-lock.json

+3
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

glean/package.json

+4
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,9 @@
4646
"package.json",
4747
"dist/**/*"
4848
],
49+
"bin": {
50+
"glean": "./dist/cli/cli.js"
51+
},
4952
"scripts": {
5053
"test": "npm run test:core && npm run test:platform && npm run test:plugins",
5154
"test:core": "ts-mocha \"tests/core/**/*.spec.ts\" --recursive",
@@ -54,6 +57,7 @@
5457
"build:test-webext": "cd tests/platform/utils/webext/sample/ && npm install && npm run build:xpi",
5558
"lint": "eslint . --ext .ts,.js,.json --max-warnings=0",
5659
"fix": "eslint . --ext .ts,.js,.json --fix",
60+
"build:cli": "tsc -p ./tsconfig/cli.json",
5761
"build:webext:lib:esm": "tsc -p ./tsconfig/webext/esm.json",
5862
"build:webext:lib:cjs": "tsc -p ./tsconfig/webext/cjs.json",
5963
"build:webext:lib:browser": "tsc -p ./tsconfig/webext/browser.json",

glean/src/cli.ts

+213
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
#!/usr/bin/env node
2+
3+
/* This Source Code Form is subject to the terms of the Mozilla Public
4+
* License, v. 2.0. If a copy of the MPL was not distributed with this
5+
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6+
7+
//require('ts-node').register();
8+
//require('./glean.ts');
9+
10+
import * as exec from "child_process";
11+
import * as fs from "fs";
12+
import * as path from "path";
13+
import { argv, platform } from "process";
14+
import { promisify } from "util";
15+
16+
// Define an async/await version of "exec".
17+
const execAsync = promisify(exec.exec);
18+
19+
// The name of the directory which will contain the Python virtual environment
20+
// used to run the glean-parser.
21+
const VIRTUAL_ENVIRONMENT_DIR = ".venv";
22+
23+
// The version of glean_parser to install from PyPI.
24+
const GLEAN_PARSER_VERSION = "2.5.0";
25+
26+
// This script runs a given Python module as a "main" module, like
27+
// `python -m module`. However, it first checks that the installed
28+
// package is at the desired version, and if not, upgrades it using `pip`.
29+
//
30+
// ** IMPORTANT**
31+
// Keep this script in sync with the one in the Glean SDK (Gradle Plugin).
32+
//
33+
// Note: Groovy doesn't support embedded " in multi-line strings, so care
34+
// should be taken to use ' everywhere in this code snippet.
35+
const PYTHON_SCRIPT = `
36+
import importlib
37+
import subprocess
38+
import sys
39+
offline = sys.argv[1] == 'offline'
40+
module_name = sys.argv[2]
41+
expected_version = sys.argv[3]
42+
try:
43+
module = importlib.import_module(module_name)
44+
except ImportError:
45+
found_version = None
46+
else:
47+
found_version = getattr(module, '__version__')
48+
if found_version != expected_version:
49+
if not offline:
50+
subprocess.check_call([
51+
sys.executable,
52+
'-m',
53+
'pip',
54+
'install',
55+
'--upgrade',
56+
f'{module_name}=={expected_version}'
57+
])
58+
else:
59+
print(f'Using Python environment at {sys.executable},')
60+
print(f'expected glean_parser version {expected_version}, found {found_version}.')
61+
sys.exit(1)
62+
try:
63+
subprocess.check_call([
64+
sys.executable,
65+
'-m',
66+
module_name
67+
] + sys.argv[4:])
68+
except:
69+
# We don't need to show a traceback in this helper script.
70+
# Only the output of the subprocess is interesting.
71+
sys.exit(1)
72+
`;
73+
74+
/**
75+
* Gets the name of the Python binary, based on the host OS.
76+
*
77+
* @returns the name of the Python executable.
78+
*/
79+
function getSystemPythonBinName(): string {
80+
return (platform === "win32") ? "python.exe" : "python3";
81+
}
82+
83+
/**
84+
* Gets the full path to the directory containing the python
85+
* binaries in the virtual environment.
86+
*
87+
* Note that this directory changes depending on the host OS.
88+
*
89+
* @param venvRoot the root path of the virtual environment.
90+
*
91+
* @returns the full path to the directory containing the python
92+
* binaries in the virtual environment.
93+
*/
94+
function getPythonVenvBinariesPath(venvRoot: string): string {
95+
if (platform === "win32") {
96+
return path.join(venvRoot, "Scripts");
97+
}
98+
99+
return path.join(venvRoot, "bin");
100+
}
101+
102+
/**
103+
* Checks if a Python virtual environment is available.
104+
*
105+
* @param venvPath the Python virtual environment directory.
106+
*
107+
* @returns `true` if the Python virtual environment exists and
108+
* is accessible, `false` otherwise.
109+
*/
110+
async function checkPythonVenvExists(venvPath: string): Promise<boolean> {
111+
console.log(`Checking for a Glean virtual environment at ${venvPath}`);
112+
113+
const venvPython =
114+
path.join(getPythonVenvBinariesPath(venvPath), getSystemPythonBinName());
115+
116+
const access = promisify(fs.access);
117+
118+
try {
119+
await access(venvPath, fs.constants.F_OK);
120+
await access(venvPython, fs.constants.F_OK);
121+
122+
return true;
123+
} catch (e) {
124+
return false;
125+
}
126+
}
127+
128+
/**
129+
* Uses the system's Python interpreter to create a Python3 virtual environment.
130+
*
131+
* @param venvPath the directory in which to create the virtual environment.
132+
*
133+
* @returns `true` if the environment was correctly created, `false` otherwise.
134+
*/
135+
async function createPythonVenv(venvPath: string): Promise<boolean> {
136+
console.log(`Creating a Glean virtual environment at ${venvPath}`);
137+
138+
const pipFilename = (platform === "win32") ? "pip3.exe" : "pip3";
139+
const venvPip =
140+
path.join(getPythonVenvBinariesPath(VIRTUAL_ENVIRONMENT_DIR), pipFilename);
141+
142+
const pipCmd = `${venvPip} install wheel`;
143+
const venvCmd = `${getSystemPythonBinName()} -m venv ${VIRTUAL_ENVIRONMENT_DIR}`;
144+
145+
for (const cmd of [venvCmd, pipCmd]) {
146+
try {
147+
await execAsync(cmd);
148+
} catch (e) {
149+
console.error(e);
150+
return false;
151+
}
152+
}
153+
154+
return true;
155+
}
156+
157+
/**
158+
* Checks if a virtual environment for running the glean_parser exists,
159+
* otherwise it creates it.
160+
*
161+
* @param projectRoot the project's root directory.
162+
*/
163+
async function setup(projectRoot: string) {
164+
const venvRoot = path.join(projectRoot, VIRTUAL_ENVIRONMENT_DIR);
165+
166+
const venvExists = await checkPythonVenvExists(venvRoot);
167+
if (venvExists) {
168+
console.log(`Using Glean virtual environment at ${venvRoot}`);
169+
} else if (!await createPythonVenv(venvRoot)){
170+
console.error(`Failed to createa a Glean virtual environment at ${venvRoot}`);
171+
}
172+
}
173+
174+
/**
175+
* Runs the glean_parser with the provided options.
176+
*
177+
* @param projectRoot the project's root directory.
178+
* @param parserArgs the list of arguments passed to this command.
179+
*/
180+
async function runGlean(projectRoot: string, parserArgs: string[]) {
181+
const venvRoot = path.join(projectRoot, VIRTUAL_ENVIRONMENT_DIR);
182+
const pythonBin = path.join(getPythonVenvBinariesPath(venvRoot), getSystemPythonBinName());
183+
const cmd = `${pythonBin} -c "${PYTHON_SCRIPT}" online glean_parser ${GLEAN_PARSER_VERSION} ${parserArgs.join(" ")}`;
184+
try {
185+
await execAsync(cmd);
186+
} catch (e) {
187+
console.error(e);
188+
}
189+
}
190+
191+
/**
192+
* Runs the command.
193+
*
194+
* @param args the arguments passed to this process.
195+
*/
196+
async function run(args: string[]) {
197+
if (args.length < 3) {
198+
throw new Error("Not enough arguments. Please refer to https://mozilla.github.io/glean_parser/readme.html");
199+
}
200+
201+
const projectRoot = process.cwd();
202+
try {
203+
await setup(projectRoot);
204+
} catch (err) {
205+
console.error("Failed to setup the Glean build environment", err);
206+
}
207+
208+
await runGlean(projectRoot, args.slice(2));
209+
}
210+
211+
run(argv).catch(e => {
212+
console.error("There was an error running Glean", e);
213+
});

glean/tsconfig/cli.json

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"extends": "./base.json",
3+
"include": [
4+
"../src/cli.ts"
5+
],
6+
"compilerOptions": {
7+
"outDir": "../dist/cli"
8+
}
9+
}

0 commit comments

Comments
 (0)