@lazycatcloud/lzc-cli 1.3.17 → 2.0.0-pre.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (128) hide show
  1. package/README.md +47 -7
  2. package/changelog.md +14 -0
  3. package/lib/app/apkshell.js +7 -44
  4. package/lib/app/index.js +178 -64
  5. package/lib/app/lpk_build.js +446 -61
  6. package/lib/app/lpk_build_images.js +749 -0
  7. package/lib/app/lpk_create.js +192 -45
  8. package/lib/app/lpk_create_generator.js +141 -13
  9. package/lib/app/lpk_devshell.js +33 -19
  10. package/lib/app/lpk_embed_images.js +257 -0
  11. package/lib/app/lpk_installer.js +17 -9
  12. package/lib/app/manifest_build.js +259 -0
  13. package/lib/app/project_cp.js +59 -0
  14. package/lib/app/project_deploy.js +58 -0
  15. package/lib/app/project_exec.js +82 -0
  16. package/lib/app/project_info.js +106 -0
  17. package/lib/app/project_log.js +62 -0
  18. package/lib/app/project_runtime.js +356 -0
  19. package/lib/app/project_start.js +95 -0
  20. package/lib/app/project_sync.js +499 -0
  21. package/lib/appstore/apkshell.js +50 -0
  22. package/lib/box/index.js +101 -4
  23. package/lib/box/ssh_remote.js +259 -0
  24. package/lib/build_remote.js +21 -0
  25. package/lib/debug_bridge.js +891 -83
  26. package/lib/docker/index.js +30 -10
  27. package/lib/i18n/locales/en/translation.json +262 -255
  28. package/lib/i18n/locales/zh/translation.json +262 -255
  29. package/lib/lpk/core.js +488 -0
  30. package/lib/lpk/index.js +210 -0
  31. package/lib/migrate/index.js +52 -0
  32. package/lib/package_info.js +135 -0
  33. package/lib/shellapi.js +35 -1
  34. package/lib/sig/core.js +254 -0
  35. package/lib/sig/index.js +88 -0
  36. package/lib/utils.js +94 -15
  37. package/package.json +3 -3
  38. package/scripts/cli.js +6 -0
  39. package/scripts/smoke/frontend-dev-entry.mjs +104 -0
  40. package/scripts/smoke/template-project.mjs +311 -0
  41. package/template/_lpk/README.md +15 -4
  42. package/template/_lpk/gui-vnc.manifest.yml.in +18 -0
  43. package/template/_lpk/hello-vue.manifest.yml.in +38 -0
  44. package/template/_lpk/manifest.yml.in +4 -11
  45. package/template/_lpk/package.yml.in +7 -0
  46. package/template/_lpk/todolist-golang.manifest.yml.in +30 -0
  47. package/template/_lpk/todolist-java.manifest.yml.in +29 -0
  48. package/template/_lpk/todolist-python.manifest.yml.in +37 -0
  49. package/template/_lpk/todolist-serverless.manifest.yml.in +38 -0
  50. package/template/_lpk/vue.lzc-build.yml.in +0 -44
  51. package/template/blank/lzc-build.dev.yml +4 -0
  52. package/template/blank/lzc-build.yml +24 -41
  53. package/template/blank/lzc-manifest.yml +7 -9
  54. package/template/blank/package.yml +7 -0
  55. package/template/golang/Dockerfile +19 -0
  56. package/template/golang/Dockerfile.dev +20 -0
  57. package/template/golang/README.md +44 -0
  58. package/template/golang/_gitignore +3 -0
  59. package/template/golang/_lzcdevignore +21 -0
  60. package/template/golang/go.mod +3 -0
  61. package/template/golang/lzc-build.dev.yml +12 -0
  62. package/template/golang/lzc-build.yml +16 -0
  63. package/template/golang/lzc-icon.png +0 -0
  64. package/template/golang/main.go +252 -0
  65. package/template/golang/manifest.dev.page.js +24 -0
  66. package/template/golang/run.sh +10 -0
  67. package/template/golang/web/index.html +238 -0
  68. package/template/gui-vnc/README.md +23 -0
  69. package/template/gui-vnc/_gitignore +2 -0
  70. package/template/gui-vnc/images/Dockerfile +30 -0
  71. package/template/gui-vnc/images/kasmvnc.yaml +33 -0
  72. package/template/gui-vnc/images/startup-script.desktop +9 -0
  73. package/template/gui-vnc/images/startup-script.sh +6 -0
  74. package/template/gui-vnc/lzc-build.dev.yml +4 -0
  75. package/template/gui-vnc/lzc-build.yml +18 -0
  76. package/template/gui-vnc/lzc-icon.png +0 -0
  77. package/template/python/Dockerfile +15 -0
  78. package/template/python/Dockerfile.dev +18 -0
  79. package/template/python/README.md +50 -0
  80. package/template/python/_gitignore +3 -0
  81. package/template/python/_lzcdevignore +21 -0
  82. package/template/python/app.py +110 -0
  83. package/template/python/lzc-build.dev.yml +12 -0
  84. package/template/python/lzc-build.yml +16 -0
  85. package/template/python/lzc-icon.png +0 -0
  86. package/template/python/manifest.dev.page.js +25 -0
  87. package/template/python/requirements.txt +1 -0
  88. package/template/python/run.sh +14 -0
  89. package/template/python/web/index.html +238 -0
  90. package/template/springboot/Dockerfile +20 -0
  91. package/template/springboot/Dockerfile.dev +20 -0
  92. package/template/springboot/README.md +44 -0
  93. package/template/springboot/_gitignore +3 -0
  94. package/template/springboot/_lzcdevignore +21 -0
  95. package/template/springboot/lzc-build.dev.yml +12 -0
  96. package/template/springboot/lzc-build.yml +16 -0
  97. package/template/springboot/lzc-icon.png +0 -0
  98. package/template/springboot/manifest.dev.page.js +24 -0
  99. package/template/springboot/pom.xml +38 -0
  100. package/template/springboot/run.sh +10 -0
  101. package/template/springboot/src/main/java/cloud/lazycat/app/Application.java +132 -0
  102. package/template/springboot/src/main/resources/application.properties +1 -0
  103. package/template/springboot/src/main/resources/static/index.html +238 -0
  104. package/template/vue/README.md +18 -21
  105. package/template/vue/lzc-build.dev.yml +7 -0
  106. package/template/vue/lzc-build.yml +30 -43
  107. package/template/vue/manifest.dev.page.js +50 -0
  108. package/template/vue/src/App.vue +36 -25
  109. package/template/vue/src/style.css +106 -49
  110. package/template/vue-minidb/README.md +26 -0
  111. package/template/vue-minidb/_gitignore +25 -0
  112. package/template/vue-minidb/index.html +13 -0
  113. package/template/vue-minidb/lzc-build.dev.yml +7 -0
  114. package/template/vue-minidb/lzc-build.yml +46 -0
  115. package/template/vue-minidb/lzc-icon.png +0 -0
  116. package/template/vue-minidb/manifest.dev.page.js +50 -0
  117. package/template/vue-minidb/package.json +21 -0
  118. package/template/vue-minidb/public/vite.svg +1 -0
  119. package/template/vue-minidb/src/App.vue +206 -0
  120. package/template/vue-minidb/src/assets/vue.svg +1 -0
  121. package/template/vue-minidb/src/main.ts +5 -0
  122. package/template/vue-minidb/src/style.css +136 -0
  123. package/template/vue-minidb/src/vite-env.d.ts +1 -0
  124. package/template/vue-minidb/tsconfig.app.json +24 -0
  125. package/template/vue-minidb/tsconfig.json +7 -0
  126. package/template/vue-minidb/tsconfig.node.json +22 -0
  127. package/template/vue-minidb/vite.config.ts +10 -0
  128. /package/template/{vue → vue-minidb}/src/components/HelloWorld.vue +0 -0
@@ -1,25 +1,17 @@
1
1
  import path from 'node:path';
2
2
  import fs from 'node:fs';
3
+ import zlib from 'node:zlib';
3
4
  import logger from 'loglevel';
4
- import {
5
- loadFromYaml,
6
- isDirExist,
7
- isDirSync,
8
- isFileExist,
9
- dumpToYaml,
10
- envTemplateFile,
11
- isValidPackageName,
12
- tarContentDir,
13
- isPngWithFile,
14
- isMacOs,
15
- isWindows,
16
- isLinux,
17
- } from '../utils.js';
5
+ import { isDirExist, isDirSync, isFileExist, dumpToYaml, envTemplateFile, isValidPackageName, tarContentDir, isPngWithFile, isMacOs, isWindows, isLinux } from '../utils.js';
18
6
  import spawn from 'cross-spawn';
19
- import { LpkManifest } from './lpk_create.js';
7
+ import { loadEffectiveManifest, splitManifestAndPackageInfo, findManifestStaticFields, PACKAGE_FILE_NAME } from '../package_info.js';
8
+ import { buildVarsFromEnvEntries, normalizeBuildEnvEntries, preprocessManifestFile } from './manifest_build.js';
20
9
  import archiver from 'archiver';
21
10
  import yaml from 'js-yaml';
11
+ import { buildConfiguredImagesToTempDir } from './lpk_build_images.js';
12
+ import { pipeline } from 'node:stream/promises';
22
13
  import { t } from '../i18n/index.js';
14
+ import mergeWith from 'lodash.mergewith';
23
15
 
24
16
  async function archiveFolderTo(appDir, out, format = 'zip') {
25
17
  return new Promise(async (resolve, reject) => {
@@ -46,6 +38,134 @@ async function archiveFolderTo(appDir, out, format = 'zip') {
46
38
  });
47
39
  }
48
40
 
41
+ async function gzipFileTo(inputPath, outputPath) {
42
+ const source = fs.createReadStream(inputPath);
43
+ const gzip = zlib.createGzip({ mtime: 0 });
44
+ const target = fs.createWriteStream(outputPath);
45
+ await pipeline(source, gzip, target);
46
+ }
47
+
48
+ function rewriteManifestEmbeddedImages(manifest, resolvedImageByAlias) {
49
+ const EMBED_PREFIX = 'embed:';
50
+ if (!resolvedImageByAlias || typeof resolvedImageByAlias !== 'object') {
51
+ return manifest;
52
+ }
53
+
54
+ const parseAlias = (rawValue) => {
55
+ const trimmed = String(rawValue ?? '').trim();
56
+ if (!trimmed.startsWith(EMBED_PREFIX)) {
57
+ return '';
58
+ }
59
+ const rest = trimmed.slice(EMBED_PREFIX.length).trim();
60
+ if (!rest) {
61
+ throw new Error(`Invalid image reference "${rawValue}", alias is required after "${EMBED_PREFIX}"`);
62
+ }
63
+ const at = rest.indexOf('@');
64
+ const alias = (at >= 0 ? rest.slice(0, at) : rest).trim();
65
+ if (!alias) {
66
+ throw new Error(`Invalid image reference "${rawValue}", alias is required after "${EMBED_PREFIX}"`);
67
+ }
68
+ return alias;
69
+ };
70
+
71
+ const walk = (value) => {
72
+ if (typeof value === 'string') {
73
+ const alias = parseAlias(value);
74
+ if (alias) {
75
+ const resolved = resolvedImageByAlias[alias];
76
+ if (!resolved) {
77
+ throw new Error(`Cannot resolve embedded image alias "${alias}" to final image reference`);
78
+ }
79
+ return `${EMBED_PREFIX}${alias}@${resolved}`;
80
+ }
81
+ return value;
82
+ }
83
+ if (Array.isArray(value)) {
84
+ for (let index = 0; index < value.length; index += 1) {
85
+ value[index] = walk(value[index]);
86
+ }
87
+ return value;
88
+ }
89
+ if (value && typeof value === 'object') {
90
+ for (const [key, item] of Object.entries(value)) {
91
+ value[key] = walk(item);
92
+ }
93
+ return value;
94
+ }
95
+ return value;
96
+ };
97
+
98
+ return walk(manifest);
99
+ }
100
+
101
+ function rewriteManifestEmbeddedImagesInText(rawText, resolvedImageByAlias) {
102
+ if (!resolvedImageByAlias || typeof resolvedImageByAlias !== 'object') {
103
+ return rawText;
104
+ }
105
+ let output = String(rawText ?? '');
106
+ for (const [alias, resolved] of Object.entries(resolvedImageByAlias)) {
107
+ if (!alias || !resolved) {
108
+ continue;
109
+ }
110
+ const escapedAlias = alias.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
111
+ const pattern = new RegExp(`embed:${escapedAlias}(?!@sha256:[0-9a-fA-F]{64})`, 'g');
112
+ output = output.replace(pattern, `embed:${alias}@${resolved}`);
113
+ }
114
+ return output;
115
+ }
116
+
117
+ function rewriteTopLevelManifestScalarInText(rawText, key, value) {
118
+ const normalizedValue = String(value ?? '').trim();
119
+ if (!normalizedValue) {
120
+ return rawText;
121
+ }
122
+ const lines = String(rawText ?? '').split('\n');
123
+ const pattern = new RegExp(`^(\\s*)${key}:(\\s*)([^#]*?)(\\s*(#.*))?$`);
124
+ let replaced = false;
125
+ const output = lines.map((line) => {
126
+ if (replaced) {
127
+ return line;
128
+ }
129
+ const match = line.match(pattern);
130
+ if (!match) {
131
+ return line;
132
+ }
133
+ replaced = true;
134
+ const indent = match[1] ?? '';
135
+ const spacing = match[2] ?? ' ';
136
+ const comment = match[4] ?? '';
137
+ return `${indent}${key}:${spacing}${normalizedValue}${comment}`;
138
+ });
139
+ return output.join('\n');
140
+ }
141
+
142
+ function removeTopLevelManifestFieldsInText(rawText, keys) {
143
+ const wanted = new Set((keys ?? []).map((key) => String(key ?? '').trim()).filter(Boolean));
144
+ if (wanted.size === 0) {
145
+ return String(rawText ?? '');
146
+ }
147
+ const lines = String(rawText ?? '').split('\n');
148
+ const output = [];
149
+ let skipping = false;
150
+
151
+ for (const line of lines) {
152
+ const trimmed = line.trim();
153
+ const indent = line.length - line.trimStart().length;
154
+ if (indent === 0 && !trimmed.startsWith('#')) {
155
+ const match = trimmed.match(/^([A-Za-z0-9_]+):/);
156
+ if (match && wanted.has(match[1])) {
157
+ skipping = true;
158
+ continue;
159
+ }
160
+ skipping = false;
161
+ }
162
+ if (!skipping) {
163
+ output.push(line);
164
+ }
165
+ }
166
+ return output.join('\n');
167
+ }
168
+
49
169
  async function fetchIconTo(options, cwd, destDir) {
50
170
  if (!options['icon']) {
51
171
  logger.warn(t('lzc_cli.lib.app.lpk_build.fetch_icon_to_icon_empty_fail', '图标icon 没有指定'));
@@ -68,7 +188,12 @@ async function fetchIconTo(options, cwd, destDir) {
68
188
  }
69
189
 
70
190
  if (!isPngWithFile(iconPath)) {
71
- logger.warn(t('lzc_cli.lib.app.lpk_build.fetch_icon_to_icon_not_is_png_fail', `图标icon {{ iconPath }} 验证失败(不是一个png格式)`, { iconPath, interpolation: { escapeValue: false } }));
191
+ logger.warn(
192
+ t('lzc_cli.lib.app.lpk_build.fetch_icon_to_icon_not_is_png_fail', `图标icon {{ iconPath }} 验证失败(不是一个png格式)`, {
193
+ iconPath,
194
+ interpolation: { escapeValue: false },
195
+ }),
196
+ );
72
197
  return;
73
198
  } else {
74
199
  logger.debug(t('lzc_cli.lib.app.lpk_build.fetch_icon_to_icon_is_png', `图标icon {{ iconPath }} 验证成功(png格式)`, { iconPath, interpolation: { escapeValue: false } }));
@@ -97,7 +222,12 @@ async function fetchLzcDeployParamTo(options, cwd, destDir) {
97
222
  }
98
223
 
99
224
  if (!isFileExist(deployParamsPath)) {
100
- logger.warn(t('lzc_cli.lib.app.lpk_build.fetch_lzc_deploy_param_to_not_exist', `deploy_params {{ deployParamsPath }} 不存在`, { deployParamsPath, interpolation: { escapeValue: false } }));
225
+ logger.warn(
226
+ t('lzc_cli.lib.app.lpk_build.fetch_lzc_deploy_param_to_not_exist', `deploy_params {{ deployParamsPath }} 不存在`, {
227
+ deployParamsPath,
228
+ interpolation: { escapeValue: false },
229
+ }),
230
+ );
101
231
  return;
102
232
  }
103
233
 
@@ -151,15 +281,126 @@ function convenientEnv() {
151
281
  );
152
282
  }
153
283
 
284
+ function formatBytes(bytes) {
285
+ const value = Number(bytes ?? 0);
286
+ if (!Number.isFinite(value) || value <= 0) {
287
+ return '0 B';
288
+ }
289
+ const units = ['B', 'KiB', 'MiB', 'GiB', 'TiB'];
290
+ let size = value;
291
+ let unitIndex = 0;
292
+ while (size >= 1024 && unitIndex < units.length - 1) {
293
+ size /= 1024;
294
+ unitIndex += 1;
295
+ }
296
+ const digits = unitIndex === 0 ? 0 : 2;
297
+ return `${size.toFixed(digits)} ${units[unitIndex]}`;
298
+ }
299
+
300
+ function mergeBuildOptions(baseOptions, topOptions) {
301
+ return mergeWith({}, baseOptions ?? {}, topOptions ?? {}, (objValue, srcValue) => {
302
+ if (Array.isArray(srcValue)) {
303
+ return srcValue;
304
+ }
305
+ return undefined;
306
+ });
307
+ }
308
+
309
+ function applyPkgIdSuffix(packageId, suffix) {
310
+ const normalizedPackageId = String(packageId ?? '').trim();
311
+ const normalizedSuffix = String(suffix ?? '').trim();
312
+ if (!normalizedSuffix) {
313
+ return normalizedPackageId;
314
+ }
315
+ if (!normalizedPackageId) {
316
+ throw new Error('package is empty, cannot apply pkg_id_suffix');
317
+ }
318
+ return `${normalizedPackageId}.${normalizedSuffix}`;
319
+ }
320
+
321
+ export function validatePackageFileForArchiveFormat(hasPackageFile, archiveFormat) {
322
+ const format = String(archiveFormat ?? '')
323
+ .trim()
324
+ .toLowerCase();
325
+ if (format === 'tar' && !hasPackageFile) {
326
+ throw new Error('package.yml is required for tar-based LPK v2 packages');
327
+ }
328
+ }
329
+
330
+ function hasBuildEnvEntries(envEntries) {
331
+ return Array.isArray(envEntries) && envEntries.length > 0;
332
+ }
333
+
334
+ export function shouldUseV2PackageLayout({ forceV2 = false, hasPackageFile = false, hasImagesConfig = false, buildEnvEntries = [] } = {}) {
335
+ return !!forceV2 || !!hasPackageFile || !!hasImagesConfig || hasBuildEnvEntries(buildEnvEntries);
336
+ }
337
+
338
+ function logDeprecatedStaticFieldWarning(manifestPath, fields) {
339
+ if (!Array.isArray(fields) || fields.length === 0) {
340
+ return;
341
+ }
342
+ logger.warn(`Building LPK v2. Static package fields in ${manifestPath} are deprecated and will be written to package.yml: ${fields.join(', ')}`);
343
+ }
344
+
345
+ function normalizeBuildOptions(options) {
346
+ const normalized = { ...(options ?? {}) };
347
+ if (Object.prototype.hasOwnProperty.call(normalized, 'envs')) {
348
+ normalized['envs'] = normalizeBuildEnvEntries(normalized['envs']);
349
+ }
350
+ return normalized;
351
+ }
352
+
353
+ function loadYamlIfExists(filePath) {
354
+ if (!filePath || !isFileExist(filePath)) {
355
+ return {};
356
+ }
357
+ return yaml.load(fs.readFileSync(filePath, 'utf8')) ?? {};
358
+ }
359
+
360
+ async function loadTemplatedYamlIfExists(filePath, env) {
361
+ if (!filePath || !isFileExist(filePath)) {
362
+ return {};
363
+ }
364
+ return yaml.load(await envTemplateFile(filePath, env)) ?? {};
365
+ }
366
+
367
+ function isDevBuildConfigName(buildConfigFile) {
368
+ return path.basename(String(buildConfigFile ?? '').trim()) === 'lzc-build.dev.yml';
369
+ }
370
+
371
+ function resolveParentBuildConfigPath(optionsFilePath) {
372
+ if (!isDevBuildConfigName(optionsFilePath)) {
373
+ return '';
374
+ }
375
+ const parentPath = path.join(path.dirname(optionsFilePath), 'lzc-build.yml');
376
+ if (!isFileExist(parentPath)) {
377
+ throw new Error(`Build config file not found: ${parentPath}`);
378
+ }
379
+ return parentPath;
380
+ }
381
+
382
+ function detectBuildProfile(buildConfigFile) {
383
+ return isDevBuildConfigName(buildConfigFile) ? 'dev' : 'release';
384
+ }
385
+
154
386
  export class LpkBuild {
155
- constructor(cwd, buildConfigFile) {
387
+ constructor(cwd, buildConfigFile, options = {}) {
156
388
  this.pwd = cwd ?? process.cwd();
389
+ this.forceV2 = !!options.forceV2;
157
390
 
158
391
  this.optionsFilePath = path.join(this.pwd, buildConfigFile);
159
- this.options = loadFromYaml(this.optionsFilePath);
392
+ this.parentOptionsFilePath = resolveParentBuildConfigPath(this.optionsFilePath);
393
+ this.options = normalizeBuildOptions(mergeBuildOptions(loadYamlIfExists(this.parentOptionsFilePath), loadYamlIfExists(this.optionsFilePath)));
394
+ this.buildProfile = detectBuildProfile(this.optionsFilePath);
160
395
 
161
396
  this.manifestFilePath = this.options['manifest'] ? path.join(this.pwd, this.options['manifest']) : path.join(this.pwd, 'lzc-manifest.yml');
397
+ this.packageFilePath = path.join(this.pwd, PACKAGE_FILE_NAME);
162
398
  this.manifest = null;
399
+ this.sourceManifest = null;
400
+ this.packageInfo = null;
401
+ this.hasPackageFile = false;
402
+ this.buildVars = null;
403
+ this.preprocessedManifestText = '';
163
404
 
164
405
  this.beforeBuildPackageFn = [];
165
406
  this.beforeDumpYamlFn = [];
@@ -169,9 +410,25 @@ export class LpkBuild {
169
410
 
170
411
  // init 时替换 lzc-build.yml 中的模板字段
171
412
  async init() {
172
- const manifest = await this.getManifest();
413
+ const sourceLoaded = loadEffectiveManifest(this.manifestFilePath, {
414
+ packageFilePath: this.packageFilePath,
415
+ });
173
416
  const primitive = convenientEnv();
174
- this.options = yaml.load(await envTemplateFile(this.optionsFilePath, Object.assign({}, primitive, manifest)));
417
+ const baseTemplateEnv = Object.assign({}, primitive, sourceLoaded.manifest);
418
+ const firstParentOptions = await loadTemplatedYamlIfExists(this.parentOptionsFilePath, baseTemplateEnv);
419
+ const firstTopOptions = await loadTemplatedYamlIfExists(this.optionsFilePath, baseTemplateEnv);
420
+ const firstOptions = normalizeBuildOptions(mergeBuildOptions(firstParentOptions, firstTopOptions));
421
+ const buildVars = buildVarsFromEnvEntries(firstOptions['envs']);
422
+ const finalTemplateEnv = Object.assign({}, baseTemplateEnv, buildVars);
423
+ const parentOptions = await loadTemplatedYamlIfExists(this.parentOptionsFilePath, finalTemplateEnv);
424
+ const topOptions = await loadTemplatedYamlIfExists(this.optionsFilePath, finalTemplateEnv);
425
+ this.options = normalizeBuildOptions(mergeBuildOptions(parentOptions, topOptions));
426
+ this.manifest = null;
427
+ this.sourceManifest = null;
428
+ this.packageInfo = null;
429
+ this.hasPackageFile = false;
430
+ this.buildVars = buildVarsFromEnvEntries(this.options['envs']);
431
+ this.preprocessedManifestText = '';
175
432
  return this;
176
433
  }
177
434
 
@@ -197,17 +454,40 @@ export class LpkBuild {
197
454
  this.beforeDumpLpkFn.push(fn);
198
455
  }
199
456
 
457
+ getBuildVars() {
458
+ if (!this.buildVars) {
459
+ this.buildVars = buildVarsFromEnvEntries(this.options['envs']);
460
+ }
461
+ return this.buildVars;
462
+ }
463
+
200
464
  async getManifest() {
201
465
  if (this.manifest) {
202
466
  return this.manifest;
203
467
  }
204
468
 
205
- let lpkM = new LpkManifest();
206
- await lpkM.init(this.manifestFilePath);
207
- this.manifest = lpkM.manifest;
208
- // manifest使用 text/template模板时,标记为异常manifest
209
- // 创建lpk时,直接将源文件拷入 lpk
210
- this.excpManifest = lpkM.excpManifest;
469
+ this.preprocessedManifestText = preprocessManifestFile(this.manifestFilePath, {
470
+ profile: this.buildProfile,
471
+ envs: this.getBuildVars(),
472
+ });
473
+
474
+ const loaded = loadEffectiveManifest(this.manifestFilePath, {
475
+ manifestText: this.preprocessedManifestText,
476
+ packageFilePath: this.packageFilePath,
477
+ });
478
+ this.excpManifest = loaded.excpManifest;
479
+ this.sourceManifest = loaded.sourceManifest;
480
+ this.packageInfo = loaded.packageInfo;
481
+ this.hasPackageFile = loaded.hasPackageFile;
482
+ this.manifest = loaded.manifest;
483
+
484
+ const packageWithSuffix = applyPkgIdSuffix(this.manifest['package'], this.options['pkg_id_suffix']);
485
+ if (packageWithSuffix !== this.manifest['package']) {
486
+ this.manifest = { ...this.manifest, package: packageWithSuffix };
487
+ if (this.hasPackageFile) {
488
+ this.packageInfo = { ...(this.packageInfo ?? {}), package: packageWithSuffix };
489
+ }
490
+ }
211
491
 
212
492
  if (!isValidPackageName(this.manifest['package'])) {
213
493
  throw t('lzc_cli.lib.app.lpk_build.get_manifest_package_name_fail', `{{ package }} 含有非法字符,请使用正确的包名格式(java的包名格式),如:cloud.lazycat.apps.video`, {
@@ -215,7 +495,7 @@ export class LpkBuild {
215
495
  });
216
496
  }
217
497
 
218
- if (!this.manifest['application']['subdomain']) {
498
+ if (!this.manifest?.application?.subdomain) {
219
499
  throw t('lzc_cli.lib.app.lpk_build.get_manifest_subdomain_empty', `application 模块下的 subdomain 字段不能为空`);
220
500
  }
221
501
 
@@ -223,6 +503,8 @@ export class LpkBuild {
223
503
  }
224
504
 
225
505
  async exec() {
506
+ await this.getManifest();
507
+
226
508
  if (this.beforeBuildPackageFn.length > 0) {
227
509
  this.options = await this.beforeBuildPackageFn.reduce(async (prev, curr) => {
228
510
  return await curr(await prev);
@@ -235,6 +517,7 @@ export class LpkBuild {
235
517
  let p = spawn.sync(cmd, [cmdArgs, this.options['buildscript']], {
236
518
  cwd: this.pwd,
237
519
  stdio: 'inherit',
520
+ env: { ...process.env, ...this.getBuildVars() },
238
521
  });
239
522
  if (p.status != 0) {
240
523
  throw t('lzc_cli.lib.app.lpk_build.exec_build_fail', `构建失败`);
@@ -245,9 +528,16 @@ export class LpkBuild {
245
528
 
246
529
  // 输出路径
247
530
  let packName = this.options['lpkPath'];
248
- const pkgout = path.resolve(this.pwd, this.options['pkgout']);
249
- if (!packName && !isDirExist(pkgout)) {
250
- throw t('lzc_cli.lib.app.lpk_build.exec_pkgout_not_exist', `{{ pkgout }} 不存在`, { pkgout, interpolation: { escapeValue: false } });
531
+ let pkgout = '';
532
+ if (!packName) {
533
+ const rawPkgout = this.options['pkgout'];
534
+ if (rawPkgout !== undefined && rawPkgout !== null && typeof rawPkgout !== 'string') {
535
+ throw t('lzc_cli.lib.app.lpk_build.exec_pkgout_invalid_type', 'pkgout must be a string when specified');
536
+ }
537
+ pkgout = path.resolve(this.pwd, typeof rawPkgout === 'string' && rawPkgout.trim() !== '' ? rawPkgout : './');
538
+ if (!isDirExist(pkgout)) {
539
+ throw t('lzc_cli.lib.app.lpk_build.exec_pkgout_not_exist', `{{ pkgout }} 不存在`, { pkgout, interpolation: { escapeValue: false } });
540
+ }
251
541
  }
252
542
 
253
543
  const tempDir = fs.mkdtempSync('.lzc-cli-build');
@@ -255,31 +545,37 @@ export class LpkBuild {
255
545
  let browserExtension = this.options['browser-extension'];
256
546
  let aiPodService = this.options['ai-pod-service'];
257
547
  try {
258
- if (contentdir) {
548
+ const hasContentDir = !!contentdir;
549
+ const needsContentArchive = hasContentDir || this.beforeTarContentFn.length > 0;
550
+ let tempContentDir = '';
551
+ if (hasContentDir) {
259
552
  contentdir = path.resolve(this.pwd, contentdir);
260
553
  if (!isDirExist(contentdir)) {
261
554
  throw `${contentdir} 不存在`;
262
555
  }
263
556
  } else {
264
557
  logger.warn(t('lzc_cli.lib.app.lpk_build.exec_skip_copy_contentdir', '跳过拷贝 contentdir 内容'));
265
- // 当没有指定的 contentdir 的时候,也生成一个空的文件夹
266
- // 原因是:可能其他地方会复制内容进来,像 devshell 中的会在打包的时候,将ssh key拷贝进来
267
- contentdir = fs.mkdtempSync(path.join(tempDir, 'fake-contentdir'));
268
558
  }
269
559
 
270
- // 开始打包 contentdir
271
- if (this.beforeTarContentFn.length > 0) {
272
- await this.beforeTarContentFn.reduce(async (prev, curr) => {
273
- let _prev = await prev;
274
- await curr(_prev, this.options);
275
- return _prev;
276
- }, contentdir);
560
+ if (needsContentArchive) {
561
+ if (!hasContentDir) {
562
+ // 没有显式 contentdir 时,仅在 hook 需要写入内容时创建临时目录。
563
+ tempContentDir = fs.mkdtempSync(path.join(tempDir, 'fake-contentdir'));
564
+ contentdir = tempContentDir;
565
+ }
566
+
567
+ if (this.beforeTarContentFn.length > 0) {
568
+ await this.beforeTarContentFn.reduce(async (prev, curr) => {
569
+ let _prev = await prev;
570
+ await curr(_prev, this.options);
571
+ return _prev;
572
+ }, contentdir);
573
+ }
574
+ await tarContentDir(['./'], path.join(tempDir, 'content.tar'), contentdir);
277
575
  }
278
- await tarContentDir(['./'], path.join(tempDir, 'content.tar'), contentdir);
279
576
 
280
- // 如果是临时的 contentdir, 目录在打包完成后删除
281
- if (!this.options['contentdir']) {
282
- fs.rmSync(contentdir, { recursive: true });
577
+ if (tempContentDir) {
578
+ fs.rmSync(tempContentDir, { recursive: true });
283
579
  }
284
580
 
285
581
  if (browserExtension) {
@@ -291,7 +587,10 @@ export class LpkBuild {
291
587
  } else if (isFileExist(browserExtension)) {
292
588
  fs.copyFileSync(browserExtension, path.join(tempDir, 'extension.zip'));
293
589
  } else {
294
- throw t('lzc_cli.lib.app.lpk_build.exec_browser_extension_not_exist', `{{ browserExtension }} 不存在`, { browserExtension, interpolation: { escapeValue: false } });
590
+ throw t('lzc_cli.lib.app.lpk_build.exec_browser_extension_not_exist', `{{ browserExtension }} 不存在`, {
591
+ browserExtension,
592
+ interpolation: { escapeValue: false },
593
+ });
295
594
  }
296
595
  }
297
596
 
@@ -300,7 +599,7 @@ export class LpkBuild {
300
599
  if (!isDirExist(aiPodService)) {
301
600
  throw t('lzc_cli.lib.app.lpk_build.exec_ai_pos_service_not_exist', `{{ aiPodService }} 不存在`, {
302
601
  aiPodService,
303
- interpolation: { escapeValue: false }
602
+ interpolation: { escapeValue: false },
304
603
  });
305
604
  }
306
605
  fs.cpSync(aiPodService, path.join(tempDir, 'ai-pod-service'), {
@@ -312,6 +611,9 @@ export class LpkBuild {
312
611
  let manifest = await this.getManifest();
313
612
  if (process.env.LZC_VERSION) {
314
613
  manifest.version = process.env.LZC_VERSION;
614
+ if (this.hasPackageFile) {
615
+ this.packageInfo = { ...(this.packageInfo ?? {}), version: process.env.LZC_VERSION };
616
+ }
315
617
  }
316
618
  if (this.beforeDumpYamlFn.length > 0) {
317
619
  manifest = await this.beforeDumpYamlFn.reduce(async (prev, curr) => {
@@ -319,19 +621,90 @@ export class LpkBuild {
319
621
  }, manifest);
320
622
  }
321
623
 
322
- if (process.env.LZC_MANIFEST_TEMPLATE) {
323
- logger.debug('copy origin manifest\n', this.manifestFilePath);
324
- fs.copyFileSync(this.manifestFilePath, path.join(tempDir, 'manifest.yml'));
624
+ if (Object.prototype.hasOwnProperty.call(this.options, 'embed_images')) {
625
+ throw new Error('embed_images is removed, please use lzc-build.yml images and manifest embed:alias');
626
+ }
627
+ if (Object.prototype.hasOwnProperty.call(this.options, 'embed_all_images')) {
628
+ throw new Error('embed_all_images is removed');
629
+ }
630
+ if (Object.prototype.hasOwnProperty.call(this.options, 'upstream_registry')) {
631
+ throw new Error('upstream_registry is renamed to upstream_match');
632
+ }
633
+ if (Object.prototype.hasOwnProperty.call(this.options, 'upstream_match') || Object.prototype.hasOwnProperty.call(this.options, 'upstream-match')) {
634
+ throw new Error('upstream_match is moved to lzc-build.yml images.<alias>.upstream-match');
635
+ }
636
+
637
+ const hasImagesConfig = !!this.options['images'];
638
+
639
+ let embeddedImageSummary = null;
640
+ if (hasImagesConfig) {
641
+ const buildResult = await buildConfiguredImagesToTempDir(this.options['images'], manifest, this.pwd, tempDir, {
642
+ remote: this.options['remote'],
643
+ });
644
+ if (buildResult.imageCount > 0) {
645
+ embeddedImageSummary = buildResult;
646
+ manifest = rewriteManifestEmbeddedImages(manifest, buildResult.resolvedImageByAlias);
647
+ }
648
+ }
649
+
650
+ const useV2PackageLayout = shouldUseV2PackageLayout({
651
+ forceV2: this.forceV2,
652
+ hasPackageFile: this.hasPackageFile,
653
+ hasImagesConfig,
654
+ buildEnvEntries: this.options['envs'],
655
+ });
656
+ if (useV2PackageLayout) {
657
+ logDeprecatedStaticFieldWarning(this.manifestFilePath, findManifestStaticFields(this.sourceManifest));
658
+ }
659
+
660
+ let manifestOutput = manifest;
661
+ let packageInfoOutput = null;
662
+ if (useV2PackageLayout) {
663
+ const split = splitManifestAndPackageInfo(manifest, this.packageInfo);
664
+ manifestOutput = split.manifest;
665
+ packageInfoOutput = split.packageInfo;
666
+ }
667
+
668
+ const archiveFormat = useV2PackageLayout ? 'tar' : 'zip';
669
+ validatePackageFileForArchiveFormat(!!packageInfoOutput, archiveFormat);
670
+ if (!useV2PackageLayout) {
671
+ logger.warn('Building legacy LPK v1 metadata layout. Run "lzc-cli migrate" to move static package fields into package.yml.');
672
+ }
673
+ if (embeddedImageSummary && embeddedImageSummary.imageCount > 0) {
674
+ const contentTar = path.join(tempDir, 'content.tar');
675
+ const contentTarGz = path.join(tempDir, 'content.tar.gz');
676
+ if (isFileExist(contentTar)) {
677
+ await gzipFileTo(contentTar, contentTarGz);
678
+ fs.rmSync(contentTar, { force: true });
679
+ }
680
+ }
681
+
682
+ if (process.env.LZC_MANIFEST_TEMPLATE && archiveFormat === 'zip') {
683
+ logger.debug('copy processed manifest\n', this.manifestFilePath);
684
+ fs.writeFileSync(path.join(tempDir, 'manifest.yml'), this.preprocessedManifestText);
325
685
  } else {
326
- logger.debug('manifest\n', manifest);
327
- // 异常的manifest,就将源文件给转到lpk内
686
+ logger.debug('manifest\n', manifestOutput);
328
687
  if (this.excpManifest) {
329
- fs.writeFileSync(path.join(tempDir, 'manifest.yml'), fs.readFileSync(this.manifestFilePath, 'utf8'));
688
+ let rawManifest = this.preprocessedManifestText || fs.readFileSync(this.manifestFilePath, 'utf8');
689
+ if (archiveFormat === 'zip') {
690
+ rawManifest = rewriteTopLevelManifestScalarInText(rawManifest, 'package', manifest.package);
691
+ rawManifest = rewriteTopLevelManifestScalarInText(rawManifest, 'version', manifest.version);
692
+ } else {
693
+ rawManifest = removeTopLevelManifestFieldsInText(rawManifest, Object.keys(packageInfoOutput ?? {}));
694
+ }
695
+ if (embeddedImageSummary?.resolvedImageByAlias) {
696
+ rawManifest = rewriteManifestEmbeddedImagesInText(rawManifest, embeddedImageSummary.resolvedImageByAlias);
697
+ }
698
+ fs.writeFileSync(path.join(tempDir, 'manifest.yml'), rawManifest);
330
699
  } else {
331
- dumpToYaml(manifest, path.join(tempDir, 'manifest.yml'));
700
+ dumpToYaml(manifestOutput, path.join(tempDir, 'manifest.yml'));
332
701
  }
333
702
  }
334
703
 
704
+ if (packageInfoOutput) {
705
+ dumpToYaml(packageInfoOutput, path.join(tempDir, PACKAGE_FILE_NAME));
706
+ }
707
+
335
708
  // compose.override.yml
336
709
  if (this.options['compose_override']) {
337
710
  dumpToYaml(this.options['compose_override'], path.join(tempDir, 'compose.override.yml'));
@@ -346,14 +719,26 @@ export class LpkBuild {
346
719
  }
347
720
 
348
721
  if (!packName) {
349
- packName = path.resolve(pkgout, `${manifest.package}-v${manifest.version}.lpk`);
722
+ const ext = '.lpk';
723
+ packName = path.resolve(pkgout, `${manifest.package}-v${manifest.version}${ext}`);
350
724
  }
351
725
 
352
- const lpkPath = await archiveFolderTo(tempDir, packName);
353
- logger.info(`${t('lzc_cli.lib.app.lpk_build.exec_output_lpk_path', '输出lpk包 {{ path }}', {
354
- path: lpkPath.path,
355
- interpolation: { escapeValue: false } // https://www.i18next.com/translation-function/interpolation#unescape
356
- })}`);
726
+ const lpkPath = await archiveFolderTo(tempDir, packName, archiveFormat);
727
+ logger.info(
728
+ `${t('lzc_cli.lib.app.lpk_build.exec_output_lpk_path', '输出lpk包 {{ path }}', {
729
+ path: lpkPath.path,
730
+ interpolation: { escapeValue: false }, // https://www.i18next.com/translation-function/interpolation#unescape
731
+ })}`,
732
+ );
733
+ if (embeddedImageSummary && embeddedImageSummary.imageCount > 0) {
734
+ logger.info('Embedded image upstream summary:');
735
+ for (const [alias, upstream] of Object.entries(embeddedImageSummary.upstreamByAlias ?? {})) {
736
+ logger.info(`- ${alias}: ${upstream || '(none, full embed)'}`);
737
+ }
738
+ logger.info(
739
+ `Embedded image layer size: ${formatBytes(embeddedImageSummary.embeddedLayerBytes)} (${embeddedImageSummary.embeddedLayerBytes} bytes, ${embeddedImageSummary.embeddedLayerCount} unique layers)`,
740
+ );
741
+ }
357
742
 
358
743
  return lpkPath.path;
359
744
  } finally {