* Copyright (c) 2025 Huawei Device Co., Ltd.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import * as fs from "fs";
import * as path from "path";
import * as os from "os";
import * as vscode from "vscode";
import {getDevecoCangjiePath, getOhosVersion} from "./utils";
export async function resolveAndWriteEnvVars(cjmpSdkHome: string): Promise<void> {
const workspaceRoot = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath;
if (!workspaceRoot) {
vscode.window.showErrorMessage("No workspace folder open.");
return;
}
const isWindows = os.platform() === "win32";
const platformKey = isWindows ? "windows" : "osx";
const buildEnvVars = isWindows
? parseBatEnvVars(workspaceRoot, cjmpSdkHome)
: parseShEnvVars(workspaceRoot, cjmpSdkHome);
const cangjieOhosEnvVars = buildCangjieOhosEnv();
const newEnv: Record<string, string> = { ...buildEnvVars, ...cangjieOhosEnvVars };
const config = vscode.workspace.getConfiguration();
const current = config.get<Record<string, string | null>>(`terminal.integrated.env.${platformKey}`) ?? {};
const merged: Record<string, string | null> = { ...current, ...newEnv };
if (isSameEnv(current, merged)) {
return;
}
try {
await config.update(`terminal.integrated.env.${platformKey}`, merged, vscode.ConfigurationTarget.Workspace);
vscode.window.showInformationMessage(`Successfully wrote ${Object.keys(newEnv).length} environment variables to settings.json`);
} catch (err) {
vscode.window.showWarningMessage(`Failed to write env to settings.json: ${err instanceof Error ? err.message : String(err)}`);
}
}
function buildCangjieOhosEnv(): Record<string, string> {
let sdkPath = getDevecoCangjiePath();
if (sdkPath === null || sdkPath === undefined) {
return {};
}
const version = getOhosVersion(sdkPath);
if (!version) {
vscode.window.showErrorMessage("Failed to read version from oh-uni-package.json.");
return {};
}
const parts = version.split(".");
const branch = `${parts[0]}.${parts[1]}`;
const isWindows = os.platform() === "win32";
let libs: Record<string, string>;
if (branch === "5.1") {
const llvmBase = path.join(sdkPath, "build", "linux_ohos_aarch64_llvm");
libs = {
AARCH64_LIBS: path.join(llvmBase, "ohos"),
AARCH64_KIT_LIBS: path.join(llvmBase, "kit"),
AARCH64_MACRO_LIBS: isWindows
? path.join(sdkPath, "build", "x86_64-w64-mingw32", "macro", "ohos")
: path.join(llvmBase, "ng_macro", "ohos"),
};
} else {
const cjnativeBase = path.join(sdkPath, "api", "lib", "linux_ohos_aarch64_cjnative");
libs = {
AARCH64_LIBS: path.join(cjnativeBase, "ohos"),
AARCH64_KIT_LIBS: path.join(cjnativeBase, "kit"),
AARCH64_MACRO_LIBS: path.join(sdkPath, "api", "macro", "ohos"),
};
}
return {
DEVECO_CANGJIE_HOME: sdkPath,
ABI: "arm64-v8a",
...libs,
};
}
function isSameEnv(existingEnv: Record<string, string | null>, mergedEnv: Record<string, string | null>): boolean {
const existingKeys = Object.keys(existingEnv);
const mergedKeys = Object.keys(mergedEnv);
if (existingKeys.length !== mergedKeys.length) {
return false;
}
for (const key of existingKeys) {
if (!Object.prototype.hasOwnProperty.call(mergedEnv, key) || existingEnv[key] !== mergedEnv[key]) {
return false;
}
}
return true;
}
function parseBatEnvVars(workspaceRoot: string, cjmpSdkHome: string): Record<string, string> {
const batPath = path.join(workspaceRoot, "build.bat");
if (!fs.existsSync(batPath)) {
throw new Error(`build.bat not found at ${batPath}`);
}
const content = fs.readFileSync(batPath, "utf-8");
const lines = content.split(/\r?\n/);
const vars: Record<string, string> = {
CJMP_SDK_HOME: cjmpSdkHome,
SCRIPT_DIR: workspaceRoot,
ANDROID_SDK_ROOT: process.env.ANDROID_SDK_ROOT || "",
CD: workspaceRoot,
};
const setPattern = /^\s*set\s+"?([A-Za-z_][A-Za-z0-9_]*)=([^"]*)"?\s*$/i;
for (const rawLine of lines) {
const line = rawLine.trim();
if (line.startsWith("@REM") || line.startsWith("REM") || line === "") {
continue;
}
const m = line.match(setPattern);
if (!m) continue;
const key = m[1];
let value = m[2];
if (/^%\d/.test(value) || /^%%/.test(value)) continue;
if (key === "file_count" || key === "copied_count") continue;
value = resolveVarsBat(value, vars);
value = normalizePathSep(value, true);
vars[key] = value;
}
const exclude = new Set([
"CD",
"file_count",
"copied_count",
"ext",
"dep_path",
"dep_file",
"pkg_name",
"so_file",
"cjo_file",
"cache_file",
"copy_cjo",
"PYTHON_SCRIPT",
]);
for (const k of exclude) delete vars[k];
return vars;
}
function parseShEnvVars(workspaceRoot: string, cjmpSdkHome: string): Record<string, string> {
const shPath = path.join(workspaceRoot, "build.sh");
if (!fs.existsSync(shPath)) {
throw new Error(`build.sh not found at ${shPath}`);
}
const content = fs.readFileSync(shPath, "utf-8");
const lines = content.split(/\r?\n/);
const vars: Record<string, string> = {
CJMP_SDK_HOME: cjmpSdkHome,
SCRIPT_DIR: workspaceRoot,
ANDROID_SDK_ROOT: process.env.ANDROID_SDK_ROOT || "",
};
const assignPattern = /^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)=["']?([^"']*)["']?\s*$/;
const exportedKeys = new Set<string>();
for (const rawLine of lines) {
const line = rawLine.trim();
if (line.startsWith("#") || line === "") continue;
const m = line.match(assignPattern);
if (!m) continue;
const key = m[1];
let value = m[2];
if (/\$\(/.test(value)) continue;
if (/^\$[0-9]/.test(value)) continue;
const isExport = /^\s*export\s+/.test(line);
if (isExport) {
exportedKeys.add(key);
}
value = resolveVarsSh(value, vars);
value = normalizePathSep(value, false);
vars[key] = value;
}
const output: Record<string, string> = {};
exportedKeys.add("CJMP_SDK_HOME");
for (const k of exportedKeys) {
if (vars[k] !== undefined) {
output[k] = vars[k];
}
}
const exclude = new Set(["SCRIPT_DIR"]);
for (const k of exclude) delete output[k];
return output;
}
function resolveVarsBat(value: string, vars: Record<string, string>): string {
let result = value;
let prevResult = "";
let maxIter = 10;
while (result !== prevResult && maxIter-- > 0) {
prevResult = result;
result = result.replace(/!([A-Za-z_][A-Za-z0-9_]*)!/g, (_, k) =>
vars[k] !== undefined ? vars[k] : `!${k}!`
);
result = result.replace(/%([A-Za-z_][A-Za-z0-9_]*)%/g, (_, k) =>
vars[k] !== undefined ? vars[k] : `%${k}%`
);
}
return result;
}
function resolveVarsSh(value: string, vars: Record<string, string>): string {
let result = value;
let prevResult = "";
let maxIter = 10;
while (result !== prevResult && maxIter-- > 0) {
prevResult = result;
result = result.replace(/\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g, (_, k) =>
vars[k] !== undefined ? vars[k] : `\${${k}}`
);
result = result.replace(
/\$([A-Za-z_][A-Za-z0-9_]*)/g,
(full, k) => (vars[k] !== undefined ? vars[k] : full)
);
}
return result;
}
function normalizePathSep(p: string, isWindows: boolean): string {
if (isWindows) {
return p.replace(/\//g, "\\");
}
return p.replace(/\\/g, "/");
}