diff --git a/arkoala/ohos-sdk/ohos-sdk/scripts/http-archive.mjs b/arkoala/ohos-sdk/ohos-sdk/scripts/http-archive.mjs new file mode 100644 index 0000000000000000000000000000000000000000..e5e44e780096b24f5a6c1845f9cca119e767e3ac --- /dev/null +++ b/arkoala/ohos-sdk/ohos-sdk/scripts/http-archive.mjs @@ -0,0 +1,194 @@ +/* + * Copyright (c) 2022-2023 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 fs from "fs" +import chalk from "chalk" +import path from "path" +import url from "url" +import os from "os" +import https from "https" +import http from "http" +import decompress from "decompress" +import tar from "tar" + +async function download(url, filePath, headers) { + const protocol = url.startsWith("https") ? https : http + + return new Promise((resolve, reject) => { + const file = fs.createWriteStream(filePath) + let fileInfo = null + + let options = { + headers: headers, + rejectUnauthorized: false, // TODO: add cert + } + + file.on('error', err => reject(err)) + file.on('open', () => { + const request = protocol.request(url, options, response => { + if (response.statusCode > 300) { + fs.unlink(filePath, () => { + reject(new Error(`Failed to get '${url}' (${response.statusCode})`)) + }) + return + } + fileInfo = { + mime: response.headers['content-type'], + size: parseInt(response.headers['content-length'], 10), + } + response.pipe(file) + }) + request.on('error', err => fs.unlink(filePath, () => reject(err))) + request.end() + }) + file.on('finish', () => resolve(fileInfo)) + }) +} + +export async function extractArchive(inPath, outPath, stripPrefix) { + const ext = path.parse(inPath).ext + switch(ext) { + case ".zip": + await extractZip(inPath, outPath, stripPrefix) + break + case ".tar": + await extractTar(inPath, outPath, stripPrefix) + break + case ".gz": + await extractTar(inPath, outPath, stripPrefix) + break + default: + + } +} + +async function extractZip(inPath, outPath, stripPrefix) { + return new Promise((resolve, reject) => { + let options = {} + if (stripPrefix) { + if (!stripPrefix.endsWith("/")) stripPrefix += "/" + options.map = file => { + if (file.path.startsWith(stripPrefix)) file.path = file.path.slice(stripPrefix.length) + return file + } + } + + decompress(inPath, outPath, options).then(() => resolve()).catch(err => reject(err)) + }) +} + +async function extractTar(inPath, outPath, stripPrefix) { + var strip = 0 + if (stripPrefix) { + if (!stripPrefix.endsWith("/")) stripPrefix += "/" + strip = stripPrefix.split("/").length - 1 + } + + await tar.x({ + file: inPath, + strip: strip, + C: outPath + }) +} + +export async function http_file(httpUrl, outDir, fileName = null, headers = null) { + console.log(chalk.green(`Downloading ${httpUrl} -> ${outDir}`)) + + fs.mkdirSync(outDir, {overwrite: true, recursive: true}) + await download(httpUrl, `${outDir}/${fileName}`, headers) +} + +function sleep(ms) { + return new Promise((resolve) => setTimeout(resolve, ms)) +} + +function basename(fileName) { + if (fileName.endsWith(".zip") || fileName.endsWith(".tgz")) return fileName.substring(0, fileName.length - 4) + else if (fileName.endsWith(".tar.gz")) return fileName.substring(0, fileName.length - 7) + else throw new Error("Unsupported file type for: " + fileName) +} + +export async function http_archive(httpUrl, outDir, stripPrefix = null, headers = null, copy = false) { + let fileName = path.basename(url.parse(httpUrl).pathname) + fileName = fileName.replaceAll(":", "_") // todo: validate the file name is correct + fs.mkdirSync(outDir, {overwrite: true, recursive: true}) + + if (process.env.HTTP_CACHE_DIR) { + let cachedFilePath = path.join(process.env.HTTP_CACHE_DIR, fileName) + let cachedDir = path.join(process.env.HTTP_CACHE_DIR, "unzip", basename(fileName)) + let lockFile = cachedFilePath + ".lock" + + while(true) { + if (fs.existsSync(cachedDir) && !fs.existsSync(lockFile)) { + if (copy) { // Make a copy + console.log(chalk.green(`Make copy to ${outDir} from ${cachedDir}`)) + fs.cpSync(cachedDir, outDir, {recursive: true}) + } else { // Make symlinks + fs.readdirSync(cachedDir).forEach(file => { + let src = path.join(cachedDir, file) + let dest = path.join(outDir, file) + console.log(chalk.green(`Make symlink to ${src} from ${dest}`)) + try { + fs.symlinkSync(src, dest) + } catch (err) { + console.log(err) + } + }) + } + break + } else { + try { + fs.mkdirSync(lockFile) + + console.log(chalk.green(`Downloading ${httpUrl} to ${cachedFilePath}`)) + await download(httpUrl, cachedFilePath, headers).catch(err => { + console.error("Download error: " + err + ", remove cachedFile:" + cachedFilePath) + fs.rmdirSync(lockFile) + fs.unlinkSync(cachedFilePath) + }) + + console.log(chalk.green(`Unzipping ${cachedFilePath} to ${cachedDir}`)) + await extractArchive(cachedFilePath, cachedDir, stripPrefix).then(() => { + console.log("Remove lock file: " + lockFile) + fs.rmdirSync(lockFile) + }) + + } catch (err) { + if (err.code == "EEXIST") { + let counter = 0 + while (fs.existsSync(lockFile)) { + await sleep(1000) + if (++counter % 5 == 0) + console.log("Waiting for " + lockFile + " to be deleted: " + counter + " secs.") + } + console.log(lockFile + " deleted in " + counter + " secs.") + } else { + throw err + } + } + } + } + } else { + console.log(chalk.green(`Downloading ${httpUrl} to ${outDir}`)) + + /* download in tmp */ + let tmp = (!process.env.BUILD_DIR) ? + path.resolve(path.join(os.tmpdir(), fileName)) : + path.resolve(path.join(process.env.BUILD_DIR, fileName)) + + await download(httpUrl, tmp, headers) + await extractArchive(tmp, outDir, stripPrefix) + } +} \ No newline at end of file diff --git a/arkoala/ohos-sdk/scripts b/arkoala/ohos-sdk/scripts new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/arkoala/sdk/scripts/ohos-app/ohconf.mjs b/arkoala/sdk/scripts/ohos-app/ohconf.mjs new file mode 100644 index 0000000000000000000000000000000000000000..02021363c247c0aee70093886b33c221e0412e81 --- /dev/null +++ b/arkoala/sdk/scripts/ohos-app/ohconf.mjs @@ -0,0 +1,354 @@ +/* + * Copyright (c) 2022-2023 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 path from "path" +import fs from "fs" +import { lazy, parseJsonSync, platformPath } from "../utils.mjs" +import { OH_SDK_COMPONENT, ohSdkInfo, platformName } from "../ohos-sdk/oh-sdk-utils.mjs" +import json5 from "json5" + +export const DEFAULT_OHCONF_PATH = path.join(process.cwd(), ".ohconf.json") + +export const PROJECT_MOD_APP = "app" +export const PROJECT_MOD_TEST = "test" + +// env variable variants in priority order +const OH_SDK_PATH_NAMES = [ + "DEVECO_SDK_HOME", + "HOS_SDK_HOME", + "OHOS_SDK_PATH" +] + +/** + * Provides access to ".ohconf.json" data specified by {@link ohConfPath}. + */ +export function ohConf(ohConfPath = DEFAULT_OHCONF_PATH, failIfAbsent = true) { + if (ohConfPath) { + ohConfPath = path.resolve(ohConfPath) + } + if (!fs.existsSync(ohConfPath)) { + if (failIfAbsent) + throw new Error(`> Can't find '.ohconf.json' by path: '${ohConfPath}'`) + ohConfPath = undefined + } + const ohConf = { + /** + * Returns this config's path. + */ + config: ohConfPath, + + /** + * Returns OpenHarmony SDK version defined in the config. + * + * @param failIfAbsent whether to fail if the version is not found + */ + sdkVersion: (failIfAbsent = true) => { + return ohConfValue(ohConfPath, "ohos_sdk_version", false, failIfAbsent) + }, + + /** + * Returns OpenHarmony SDK path. + * + * @param failIfAbsent whether to fail if SDK is not found + */ + sdkPath: (failIfAbsent = true) => { + let result + if (ohConf.config) { + result = ohConf.value("ohos_sdk_path", true, failIfAbsent) + if (fs.existsSync(result)) + return result + } + for (const name of OH_SDK_PATH_NAMES) { + result = platformPath(process.env[name]) + if (fs.existsSync(result)) + return result + } + if (failIfAbsent) + throw new Error(`> Can't find OpenHarmony SDK path in '.ohconf.json' '${ohConf.config}'`) + return undefined + }, + + /** + * Returns the path to the HDC tool in the OpenHarmony SDK. + * + * @param failIfAbsent whether to fail if HDC is not found + */ + hdcPath: (failIfAbsent = true) => { + const sdkPath = ohConf.sdkPath() + const sdkInfo = ohSdkInfo(sdkPath, OH_SDK_COMPONENT.toolchains) + const hdcName = "hdc" + let hdc = path.join(sdkInfo.path, hdcName) + hdc = platformName() === "windows" ? `${hdc}.exe` : hdc + if (failIfAbsent && !fs.existsSync(hdc)) + throw new Error(`> Can't find HDC in '${sdkInfo.path}'`) + return hdc + }, + + /** + * Returns a value of the {@link key} in the ["path"] group. + * + * @param key a single string key, or an array of string keys + * @param failIfAbsent whether to fail if the key is not found + */ + path: (key, failIfAbsent = true) => { + if (!ohConf.config) { + if (failIfAbsent) + throw new Error(`> Can't find path for key: '${key}'`) + return undefined + } + const keys = key.constructor === Array ? ["path", ...key] : ["path", key] + return ohConfValue(ohConfPath, keys, true, failIfAbsent) + }, + + /** + * Returns a value of the {@link key} in the ["name"] group. + * + * @param key a single string key, or an array of string keys + * @param failIfAbsent whether to fail if the key is not found + */ + name: (key, failIfAbsent = true) => { + if (!ohConf.config) { + if (failIfAbsent) + throw new Error(`> Can't find name for key: '${key}'`) + return undefined + } + const keys = key.constructor === Array ? ["name", ...key] : ["name", key] + return ohConfValue(ohConfPath, keys, false, failIfAbsent) + }, + + /** + * Returns a value of the {@link key}. + * + * @param key a single string key, or an array of string keys + * @param asPath whether to treat the value as path + * @param failIfAbsent whether to fail if the key is not found + */ + value: (key, asPath = false, failIfAbsent = true) => { + return ohConfValue(ohConfPath, key, asPath, failIfAbsent) + } + } + return ohConf +} + +/** + * Returns {@link ohConf} with default path. + */ +export function ohConfDefault(failIfAbsent = true) { + return ohConf(DEFAULT_OHCONF_PATH, failIfAbsent) +} + +function ohConfValue(ohConfPath, key, asPath = false, failIfAbsent = true) { + if (!ohConfPath) { + if (failIfAbsent) + throw new Error(`> Can't find value for key: '${key}'`) + return undefined + } + const parsePath = (pathValue) => { + if (path.isAbsolute(pathValue)) return pathValue + const root = path.resolve(path.dirname(ohConfPath)) + return path.normalize(path.join(root, pathValue)) + } + const content = parseJsonSync(ohConfPath) + let value = undefined + + if (key.constructor === Array) { + try { + value = content + key.forEach(k => value = value[k]) + } catch (e) { + } + } + else { + value = content[key] + } + if (!value) { + if (failIfAbsent) + throw new Error(`> Can't find [${key}] in '.ohconf.json' '${ohConfPath}'`) + return undefined + } + if (value.constructor === Array) { + const arr = [] + value.forEach(v => arr.push(asPath ? parsePath(v) : v)) + return arr + } + if (value.startsWith("$ohconf:")) { + const refOhConfPath = path.join(path.dirname(ohConfPath), value.substring("$ohconf:".length)) + return ohConfValue(refOhConfPath, key, asPath, failIfAbsent) + } + if (asPath) { + return parsePath(value) + } + return value +} + +/** + * Supplements {@link ohConf} data with OH project settings. + * + * It's possible that ".ohconf.json" does not exist at all, in which case only OH project settings are taken into account. + * + * @param ohConfPath path to ".ohconf.json" + * @param ohProjectRoot optional OH project root (defaults to "ohConfPath" dirname, or CWD) + */ +export function ohProjectConf(ohConfPath = DEFAULT_OHCONF_PATH, ohProjectRoot = undefined) { + const _ohConf = ohConf(ohConfPath, false) + if (!_ohConf.config) { + if (!ohProjectRoot) + ohProjectRoot = process.cwd() + + // rely on OH project settings only + if (!fs.existsSync(path.join(ohProjectRoot, "build-profile.json5")) || + !fs.existsSync(path.join(ohProjectRoot, "AppScope/app.json5"))) { + throw new Error(`> No '.ohconf.json' found (try '--oh-conf' option) and the OpenHarmony project root is invalid: '${ohProjectRoot}'`) + } + } + else if (!ohProjectRoot) { + ohProjectRoot = path.dirname(_ohConf.config) + } + + const lzModuleName = lazy(() => { + const data = fs.readFileSync(path.join(ohProjectRoot, "build-profile.json5"), { encoding: 'utf8' }) + const content = json5.parse(data) + return content["modules"][0]["name"] + }) + + const lzModulePath = lazy(() => { + const data = fs.readFileSync(path.join(ohProjectRoot, "build-profile.json5"), { encoding: 'utf8' }) + const content = json5.parse(data) + return content["modules"][0]["srcPath"] + }) + + const lzBundleName = lazy(() => { + const data = fs.readFileSync(path.join(ohProjectRoot, "AppScope/app.json5"), { encoding: 'utf8' }) + const content = json5.parse(data) + return content["app"]["bundleName"] + }) + + const lzAppAbilityName = lazy(() => { + const data = fs.readFileSync(path.join(ohProjectRoot, `${lzModulePath()}/src/main/module.json5`), { encoding: 'utf8' }) + const content = json5.parse(data) + return content["module"]["abilities"][0]["name"] + }) + + const lzTestAbilityName = lazy(() => { + const data = fs.readFileSync(path.join(ohProjectRoot, `${lzModulePath()}/src/ohosTest/module.json5`), { encoding: 'utf8' }) + const content = json5.parse(data) + return content["module"]["abilities"][0]["name"] + }) + + return { + /** + * OH project root + */ + root: ohProjectRoot, + + /** + * OH project main module name + */ + moduleName: (mod = PROJECT_MOD_APP) => lzModuleName() + (mod === PROJECT_MOD_TEST ? "_test" : ""), + + /** + * OH project main module src path (relative to the project root) + */ + modulePath: () => lzModulePath(), + + /** + * OH project bundle name + */ + bundleName: () => lzBundleName(), + + /** + * OH project main ability name + * + * @param mod project mode: "app", "test" + */ + abilityName: (mod = PROJECT_MOD_APP) => { + if (mod === PROJECT_MOD_APP) + return lzAppAbilityName() + if (mod === PROJECT_MOD_TEST) + return lzTestAbilityName() + return undefined + }, + + /** + * OH project HAP file path + * + * @param mod project mode: "app", "test" + */ + hapPath: (mod = PROJECT_MOD_APP) => { + if (mod === PROJECT_MOD_APP) + return path.join(ohProjectRoot, + `${lzModulePath()}/build/default/outputs/default/${lzModuleName()}-default-signed.hap`) + if (mod === PROJECT_MOD_TEST) + return path.join(ohProjectRoot, + `${lzModulePath()}/build/default/outputs/ohosTest/${lzModuleName()}-ohosTest-signed.hap`) + return undefined + }, + + /** + * @override + */ + config: _ohConf.config, + + /** + * @override + */ + sdkVersion: (failIfAbsent = true) => { + return _ohConf.sdkVersion(failIfAbsent) + }, + + /** + * @override + */ + sdkPath: (failIfAbsent = true) => { + return _ohConf.sdkPath(failIfAbsent) + }, + + /** + * @override + */ + hdcPath: (failIfAbsent = true) => { + return _ohConf.hdcPath(failIfAbsent) + }, + + /** + * @override + */ + path: (key, failIfAbsent = true) => { + return _ohConf.path(key, failIfAbsent) + }, + + /** + * @override + */ + name: (key, failIfAbsent = true) => { + return _ohConf.name(key, failIfAbsent) + }, + + /** + * @override + */ + value: (key, asPath = false, failIfAbsent = true) => { + return _ohConf.value(key, asPath, failIfAbsent) + } + } +} + +export function ensureExists(fileOrDirPath, create = false) { + if (create) + fs.mkdirSync(fileOrDirPath, { recursive: true }) + if (!fs.existsSync(fileOrDirPath)) + throw new Error(`> Can't find '${fileOrDirPath}'`) + return fileOrDirPath +} diff --git a/arkoala/sdk/scripts/ohos-sdk/oh-sdk-utils.mjs b/arkoala/sdk/scripts/ohos-sdk/oh-sdk-utils.mjs new file mode 100644 index 0000000000000000000000000000000000000000..2f793fae8b8a22fd44fca3b3fe8108dbc13f72fa --- /dev/null +++ b/arkoala/sdk/scripts/ohos-sdk/oh-sdk-utils.mjs @@ -0,0 +1,275 @@ +/* + * Copyright (c) 2022-2023 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 path from "path" +import fs from "fs" +import chalk from "chalk" +import os from "os" +import { ohConf, ohConfDefault } from "../ohos-app/ohconf.mjs" +import { copyDirSync, moveDirSync, parseJsonSync } from "../utils.mjs" + +/** + * Current ETS API version. + */ +export const ETS_API_VERSION = 12 + +/** + * Max OpenHarmony legacy SDK version. + */ +export const MAX_LEGACY_OHSDK_VERSION = { + major: 5, + minor: 0, + patch: 0, + revision: 24 +} + +/** + * OpenHarmony SDK component. + */ +export const OH_SDK_COMPONENT = { + ets: "ets", + js: "js", + native: "native", + toolchains: "toolchains", + previewer: "previewer" +} + +const OH_UNI_PACKAGE_NAME = "oh-uni-package.json" + +/** + * Downloads OpenHarmony SDK. + * + * @param dstPath path to download + * @param sdkComponent optional SDK component to identify the SDK archive + * @param sdkVersion SDK version, or default + * @param logIfExists logs if SDK already exists at the provided {@link dstPath} + */ +export async function downloadOhSdk( + dstPath, + sdkVersion, + sdkComponent = undefined, + logIfExists = true +) { + const sdkName = sdkComponent ? `SDK/${sdkComponent}` : "SDK" + + if (fs.existsSync(dstPath)) { + let currentSdkVersion = ohSdkInfo(dstPath, OH_SDK_COMPONENT.ets, false).version + if (currentSdkVersion === sdkVersion) { + if (logIfExists) log(`> OpenHarmony ${sdkName} ${currentSdkVersion} is found at ${dstPath}`) + return false + } + let archSdk = path.normalize(path.join(dstPath, `../${path.basename(dstPath)}.${currentSdkVersion}`)) + + let n = 1 + while (fs.existsSync(archSdk)) archSdk = `${archSdk}(${n++})` + + log(`> Saving current OpenHarmony ${sdkName} ${currentSdkVersion} to '${archSdk}'...`) + if (!moveDirSync(dstPath, archSdk)) { + error(`> Failed to remove '${dstPath}'! Please do it manually.`) + return false + } + } + + log(`> Downloading OpenHarmony ${sdkName} ${sdkVersion}...`) + const packageVersion = ohSdkVersionToPackageVersion(sdkVersion) + let packageName = sdkComponent ? "ohos-sdk-" + sdkComponent : "ohos-sdk" + let fileName = sdkComponent ? + `${packageName}-${sdkVersion}.zip` : + `${packageName}-${sdkVersion}-${platformName()}.zip` + + if (process.env.KOALA_BZ == true) { + const { generic_package_archive_openlab } = await import("../openlab/openlab.mjs") + await generic_package_archive_openlab( + packageName, + packageVersion, + fileName, + dstPath, + null, + // We patch ohos-sdk-ets directory, + // so can't use symlink here. + // It is lightweight anyways. + sdkComponent == OH_SDK_COMPONENT.ets + ) + } else { + const { generic_package_archive_gitlab } = await import("../gitlab/gitlab.mjs") + await generic_package_archive_gitlab( + packageName, + packageVersion, + fileName, + dstPath, + null, + // We patch ohos-sdk-ets directory, + // so can't use symlink here. + // It is lightweight anyways. + sdkComponent == OH_SDK_COMPONENT.ets + ) + } + return true +} + +/** + * Downloads OpenHarmony SDK ETS API to {@link dstPath}. + */ +export async function downloadOhSdkEtsApi(dstPath, version) { + if (fs.existsSync(dstPath)) { + if (version !== ohSdkVersion(dstPath, false)) { + try { + fs.rmSync(dstPath, {recursive: true, force: true}) + } catch (e) { + error(`> Failed to remove '${dstPath}'! Please do it manually.`) + return false + } + } else { + log(`> OpenHarmony SDK API ${version} is found at ${dstPath}`) + return + } + } + const ohSdkFileName = "ohos-sdk." + process.pid + let tmp = (!process.env.BUILD_DIR) ? + path.resolve(path.join(os.tmpdir(), ohSdkFileName)) : + path.resolve(path.join(process.env.BUILD_DIR, ohSdkFileName)) + + + try { + await downloadOhSdk(tmp, version, OH_SDK_COMPONENT.ets, false) + const sdkInfo = ohSdkInfo(tmp, OH_SDK_COMPONENT.ets) + + log(`> Copying OpenHarmony SDK API ${version} to '${dstPath}'`) + fs.mkdirSync(dstPath) + const etsPath = sdkInfo.path + const srcApi = path.join(etsPath, "api") + const dstApi = path.join(dstPath, "api") + if (copyDirSync(srcApi, dstApi, true)) { + const ohUniPackagePath = path.join(etsPath, OH_UNI_PACKAGE_NAME) + if (fs.existsSync(ohUniPackagePath)) { + fs.copyFileSync(ohUniPackagePath, path.join(dstPath, OH_UNI_PACKAGE_NAME)) + } + } + } finally { + fs.rmSync(tmp, { recursive: true, force: true }) + } +} + +/** + * Returns OpenHarmony SDK info: + * path: a path to the requested {@link OH_SDK_COMPONENT} + * version: SDK version + * legacy: whether the SDK version is legacy + * + * @see MAX_LEGACY_OHSDK_VERSION + */ +export function ohSdkInfo(sdkPath, sdkComponent = OH_SDK_COMPONENT.ets, failIfUnknown = true) { + const openHarmonyNextNames = [ + "HarmonyOS-NEXT-DB1", + "HarmonyOS-NEXT-DP1", + "HarmonyOS-NEXT-DP2" + ] + openHarmonyNextNames.forEach(dirName => { + const openHarmonyNextPath = path.join(sdkPath, dirName) + if (fs.existsSync(openHarmonyNextPath)) { + sdkPath = openHarmonyNextPath + return + } + }) + + const basePath = path.join(sdkPath, "openharmony") + const hmsPath = path.join(sdkPath, "hms") // Maybe use + + let sdkBaseComponentPath = path.join(basePath, `${sdkComponent}`) + let sdkHmsComponentPath = path.join(hmsPath, `${sdkComponent}`) + let version = undefined + + version = ohSdkVersion(path.join(basePath, OH_SDK_COMPONENT.ets), false) + + return { + path: sdkBaseComponentPath, + hmsPath: sdkHmsComponentPath, + version: version, + } +} + +function ohSdkVersion(sdkEtsPath, failIfUnknown = true) { + const ohUniPackagePath = path.join(sdkEtsPath, OH_UNI_PACKAGE_NAME) + if (!fs.existsSync(ohUniPackagePath)) { + if (failIfUnknown) + throw Error(`OpenHarmony SDK '${OH_UNI_PACKAGE_NAME}' is not found at: ${sdkEtsPath}`) + return undefined + } + const data = parseJsonSync(ohUniPackagePath) + return data["version"] +} + +/** + * Returns OpenHarmony SDK version object. + */ +export function parseOhSdkVersion(ohSdkVersion, failIfUnknown = true) { + const match = ohSdkVersion.match(/^(\d+).(\d+).(\d+).(\d+)$/) + if (match) { + const version = match.splice(1) + return { + major: parseInt(version[0]), + minor: parseInt(version[1]), + revision: parseInt(version[2]), + patch: parseInt(version[3]) + } + } + if (failIfUnknown) + throw Error(`unknown OpenHarmony SDK version: ${ohSdkVersion}`) + return undefined +} + +/** + * Transforms OpenHarmony SDK version to gitlab package version format. + */ +export function ohSdkVersionToPackageVersion(ohSdkVersion, failIfUnknown = true) { + const version = parseOhSdkVersion(ohSdkVersion, failIfUnknown) + return `${version.major}.${version.minor}.${version.revision}${version.patch}` +} + +/** + * Returns path to 'hdc' tool. + */ +export function hdc(failIfAbsent = true) { + const sdkPath = ohConfDefault(false).sdkPath() + const sdkInfo = ohSdkInfo(sdkPath, OH_SDK_COMPONENT.toolchains) + const hdcName = "hdc" + let hdc = path.join(sdkInfo.path, hdcName) + hdc = platformName() === "windows" ? `${hdc}.exe` : hdc + if (failIfAbsent && !fs.existsSync(hdc)) { + console.error(`> hdc not found in '${sdkInfo.path}'`) + process.exit(1) + } + return hdc +} + +/** + * Returns platform name. + */ +export function platformName() { + switch (process.platform) { + case "win32": return "windows" + case "linux": return "linux" + case "darwin": return "macos" + default: throw Error("unsupported platform " + process.platform) + } +} + +function log(msg) { + console.log(chalk.green(msg)) +} + +function error(msg) { + console.error(chalk.red(msg)) +} diff --git a/arkoala/sdk/scripts/openlab/openlab.mjs b/arkoala/sdk/scripts/openlab/openlab.mjs new file mode 100644 index 0000000000000000000000000000000000000000..19323d4f287fc5cc917c5b76855934e8e0fcc7eb --- /dev/null +++ b/arkoala/sdk/scripts/openlab/openlab.mjs @@ -0,0 +1,85 @@ +/* + * Copyright (c) 2022-2023 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 path from "path" +import process from "process" +import chalk from "chalk" +import { http_archive, http_file } from "../../../ohos-sdk/scripts/http-archive.mjs" +import { execCmd } from "../utils.mjs" + +const OPENLAB_USERNAME = process.env.OPENLAB_USERNAME +const OPENLAB_PASSWORD = process.env.OPENLAB_PASSWORD + +function getAuth() { + + if (OPENLAB_USERNAME && OPENLAB_PASSWORD) { + return { user: OPENLAB_USERNAME, password: OPENLAB_PASSWORD } + } else { + console.log(chalk.red("set OPENLAB_USERNAME and OPENLAB_PASSWORD as enviroment variables")) + } + +} + +function getHeaders() { + const {user, password} = getAuth() + const authStr = `${user}:${password}` + const auth = `Basic ${btoa(authStr)}` + return { + 'Authorization': auth, + } +} + + +function get_generic_package_url(name, version, file) { + const host = 'https://nexus.bz-openlab.ru:10443/repository/koala-raw/' + return host + name + '/' + version + '/' + file +} + +export async function generic_package_file_openlab(packageName, packageVersion, fileName, outDir, outFile) { + await http_file( + get_generic_package_url(packageName, packageVersion, fileName), + outDir, + outFile, + getHeaders() + ) +} + +export async function generic_package_archive_openlab(packageName, packageVersion, fileName, outDir, stripPrefix=null, copy=false) { + await http_archive( + get_generic_package_url(packageName, packageVersion, fileName), + outDir, + stripPrefix, + getHeaders(), + copy + ) +} + +export async function download_archive_from_url_openlab(url, outDir, stripPrefix=null) { + await http_archive( + url, + outDir, + stripPrefix, + getHeaders() + ) +} + +export async function generic_package_upload_openlab(packageName, packageVersion, filePath) { + const fileName = path.basename(filePath) + return execCmd("curl", [ + "--user", `\"${OPENLAB_USERNAME}:${OPENLAB_PASSWORD}\"`, + "--upload-file", filePath, + `https://nexus.bz-openlab.ru:10443/repository/koala-raw/${packageName}/${packageVersion}/${fileName}` + ]) +} \ No newline at end of file diff --git a/arkoala/sdk/utils.mjs b/arkoala/sdk/utils.mjs new file mode 100644 index 0000000000000000000000000000000000000000..62385324073f0eef0d810999450eccc60b2d7bb3 --- /dev/null +++ b/arkoala/sdk/utils.mjs @@ -0,0 +1,378 @@ +/* + * Copyright (c) 2022-2023 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 path from "path" +import fs from "fs" +import { spawn, spawnSync } from "child_process" + +export function capitalize(str) { + return str[0].toUpperCase() + str.slice(1) +} + +export function relativePath(from, to, posix = true) { + const relative = path.relative(from ,to) + return relative === "" ? "." : posix ? posixPath(relative) : relative +} + +export function posixPath(path) { + if (!path) return path + return process.platform === "win32" ? path.replace(/\\/g, "/") : path +} + +export function winPath(path) { + if (!path) return path + + path = path.replace(/\//g, "\\") + if (/^[a-zA-Z]:.*$/.test(path)) { + return path + } + if (/^\\[a-zA-Z]\\.*$/.test(path)) { + path = path.substring(1) + path = path.slice(0, 1) + ":" + path.slice(1) + } + return path +} + +/** + * Converts the {@code path} to the current platform format. + */ +export function platformPath(path) { + return process.platform === "win32" ? winPath(path) : posixPath(path) +} + +/** + * Copies a directory recursively. + * + * @param srcDirPath the source dir path + * @param dstDirPath the destination dir path + * @param logException whether to log an exception if it's thrown + * @return true if success, false otherwise + */ +export function copyDirSync(srcDirPath, dstDirPath, logException = false) { + return copyOrMoveDirSync(srcDirPath, dstDirPath, false, () => true, logException) +} + +/** + * Copies a directory recursively with filter: (src, dst) => boolean + * + * @see copyDirSync + */ +export function copyDirFilterSync(srcDirPath, dstDirPath, filter, logException = false) { + return copyOrMoveDirSync(srcDirPath, dstDirPath, false, filter, logException) +} + +/** + * Moves a directory. + * + * @param srcDirPath the source dir path + * @param dstDirPath the destination dir path + * @param logException whether to log an exception if it's thrown + * @return true if success, false otherwise + */ +export function moveDirSync(srcDirPath, dstDirPath, logException = false) { + return copyOrMoveDirSync(srcDirPath, dstDirPath, true, () => true, logException) +} + +/** + * Moves a directory with filter: (src, dst) => boolean + * + * @see moveDirSync + */ +export function moveDirFilterSync(srcDirPath, dstDirPath, filter, logException = false) { + return copyOrMoveDirSync(srcDirPath, dstDirPath, true, filter, logException) +} + +function copyOrMoveDirSync(srcDirPath, dstDirPath, removeSrcDir, filter = (src, dst) => true, logException = false) { + try { + fs.mkdirSync(dstDirPath, { recursive: true }) + fs.cpSync(srcDirPath, dstDirPath, { recursive: true, force: true, filter: filter }) + if (removeSrcDir) fs.rmSync(srcDirPath, { recursive: true, force: true }) + } catch (e) { + if (logException) console.log(e) + return false + } + return true +} + +/** + * Check a directory for empty + * + * @param path the source dir path + * @returns true if empty + */ +export function isEmptyDir(path) { + return fs.readdirSync(path).length === 0 +} + +/** + * Parses {@link jsonFilePath} and returns the parsed data object. + */ +export function parseJsonSync(jsonFilePath) { + return JSON.parse(fs.readFileSync(jsonFilePath, 'utf8')) +} + +// BEGIN "LAZY" + +/** + * Creates a new lazy pool which can be used to release lazy values added to it. + */ +export function createLazyPool() { + const pool = { + list: [], + + /** + * Creates a new lazy value and adds it to this pool. + */ + lazy: initializer => { + const func = lazy(initializer) + pool.add(func) + return func + }, + + /** + * Adds a lazy value to this pool. + */ + add: lazyFunc => pool.list.push(lazyFunc), + + /** + * Releases all the lazy values addded to this pool. + * + * @see releaseLazyValue + */ + release: () => pool.list.forEach(lazyFunc => releaseLazyValue(lazyFunc)) + } + return pool +} + +const DEFAULT_LAZY_POOL = createLazyPool() + +/** + * Returns default lazy pool. + * + * @see lazy + */ +export function defaultLazyPool() { + return DEFAULT_LAZY_POOL +} + +/** + * Creates a lazy value and adds it to the default lazy pool. + * + * Usage: + * + * const myValue = lazy(() => computeMyValue()) + * const useMyValue = myValue() // computes and returns the value + * const anotherUseMyValue = myValue() // just returns the value + * + * const myValue2 = lazy(initArg => computeMyValue2(initArg)) + * const useMyValue2 = myValue2("alpha") // inits, computes and returns the value + * const anotherUseMyValue2 = myValue2() // just returns the value + * + * @see defaultLazyPool + */ +export function lazy(initializer) { + return lazyImpl(initializer) +} + +/** + * Creates a lazy value with async initializer. + * + * @see lazy + */ +export function lazyAsync(initializerAsync) { + return lazyImplAsync(initializerAsync) +} + +function lazyImpl(initializer) { + const holder = { + value: undefined, + initialized: false, + + init: (arg = undefined) => { + holder.value = initializer(arg) + holder.initialized = true + }, + get: (arg = undefined) => { + if (!holder.initialized) + holder.init(arg) + return holder.value + }, + release: () => { + holder.value = undefined + holder.initialized = false + } + } + function LazyFunc(arg = undefined, __holderReceiver = undefined) { + if (__holderReceiver) { + __holderReceiver(holder) + return undefined + } + return holder.get(arg) + } + DEFAULT_LAZY_POOL.add(LazyFunc) + return LazyFunc +} + +function lazyImplAsync(initializerAsync) { + const holder = { + value: undefined, + initialized: false, + + initAsync: async (arg = undefined) => { + return new Promise(resolve => { + initializerAsync(arg).then(value => { + holder.initialized = true + holder.value = value + resolve(value) + }) + }) + }, + getAsync: async (arg = undefined) => { + if (!holder.initialized) { + return new Promise(resolve => { + holder.initAsync(arg).then(value => resolve(value)) + }) + } + return holder.value + }, + release: () => { + holder.value = undefined + holder.initialized = false + } + } + async function LazyFuncAsync(arg = undefined, __holderReceiver = undefined) { + if (__holderReceiver) { + __holderReceiver(holder) + return undefined + } + return holder.getAsync(arg) + } + DEFAULT_LAZY_POOL.add(LazyFuncAsync) + return LazyFuncAsync +} + +/** + * Forces the lazy value to re-initialize on the next read. + */ +export function releaseLazyValue(lazyFunc) { + lazyFunc(undefined, holder => holder.release()) +} + +/** + * Returns whether the lazy value has been initialized. + */ +export function lazyValueInitialized(lazyFunc) { + let initialized = false + lazyFunc(undefined, holder => initialized = holder.initialized) + return initialized +} + +// END "LAZY" + +/** + * Executes a command and returns a promise with its stdout as string. + * @options { workdir, syncMode instantLogging, onExit: (stdout, stderr): void, onError: (err): void } + */ +export async function execCmd(cmd, cmdArgs, options) { + options = { + workdir: process.cwd(), + instantLogging: false, + shell: true, + onExit: (stdout, stderr) => noop(stdout, stderr), + onError: (err) => options.ignoreError ? noop() : new Error(`> '${cmd} ${cmdArgs.join(" ")}' failed: ${err}`), + ignoreError: false, + logCmdArgs: false, + ...options + } + if (options.logCmdArgs) { + console.log(`> [exec] ${cmd} ${cmdArgs?.join(" ")}`) + } + if (options.syncMode) { + (() => { + spawnSync(cmd, cmdArgs, { + stdio: 'inherit', + shell: options.shell, + cwd: options.workdir + }) + })() + return Promise.resolve(undefined) + } + + const promise = new Promise((resolve, reject) => { + const proc = spawn(cmd, cmdArgs, { + shell: options.shell, + cwd: options.workdir + }) + + let stdout = "" + let stderr = "" + proc.stdout.on('data', data => { + const dataStr = data.toString() + stdout += dataStr + if (options.instantLogging) process.stdout.write(dataStr) + }) + proc.stderr.on('data', data => { + const dataStr = data.toString() + stderr += dataStr + if (options.instantLogging) process.stderr.write(dataStr) + }) + + proc.on("exit", code => { + options.onExit(stdout, stderr) + if (code === 0 || options.ignoreError) + resolve(stdout.trim()) + else if (code == null) + reject(options.onError("code: undefined")) + else + reject(options.onError(`code: ${code}; stderr: ${stderr}`)) + }) + if (!options.ignoreError) + proc.on("error", err => reject(options.onError(err))) + }) + return options.ignoreError ? promise.catch(() => {}) : promise +} + +/** + * extract archives of zip and tgz format from the inPath + * if no output path is specified, the current default directory is used + */ +export async function extract(inPath, outPath, stripPrefix) { + return new Promise((resolve, reject) => { + let options = {} + if (stripPrefix) { + if (!stripPrefix.endsWith("/")) stripPrefix += "/" + options.map = file => { + if (file.path.startsWith(stripPrefix)) file.path = file.path.slice(stripPrefix.length) + return file + } + } + if (!outPath) outPath = path.parse(inPath).dir + + import("decompress").then((module => { + module.default(inPath, outPath, options).then(() => resolve()).catch(err => reject(err)) + })) + }) +} + +export function splitLines(content) { + return content.split(/\r?\n/) +} + +export function splitFileLines(file) { + const content = fs.readFileSync(file, { encoding: 'utf-8' }) + return splitLines(content) +} + +export function noop(...args) {} \ No newline at end of file