From 7d8ae040fde5df26101d3be4ae9d284d0e57d4ff Mon Sep 17 00:00:00 2001 From: Joseph Garrone Date: Sat, 24 Aug 2024 23:13:16 +0200 Subject: [PATCH] Bundle JAR WPI --- src/bin/keycloakify/buildJars/buildJar.ts | 38 +++-- .../buildJars/bundleExtensionsIntoJar.ts | 137 ++++++++++++++++++ .../generateResourcesForMainTheme.ts | 3 +- src/bin/shared/buildContext.ts | 77 ++++++---- .../shared/downloadKeycloakDefaultTheme.ts | 2 +- src/bin/start-keycloak/start-keycloak.ts | 2 +- src/bin/tools/downloadAndExtractArchive.ts | 28 +--- 7 files changed, 217 insertions(+), 70 deletions(-) create mode 100644 src/bin/keycloakify/buildJars/bundleExtensionsIntoJar.ts diff --git a/src/bin/keycloakify/buildJars/buildJar.ts b/src/bin/keycloakify/buildJars/buildJar.ts index 59a692cc..4ccc9068 100644 --- a/src/bin/keycloakify/buildJars/buildJar.ts +++ b/src/bin/keycloakify/buildJars/buildJar.ts @@ -17,15 +17,20 @@ import { isInside } from "../../tools/isInside"; import child_process from "child_process"; import { rmSync } from "../../tools/fs.rmSync"; import { writeMetaInfKeycloakThemes } from "../../shared/metaInfKeycloakThemes"; +import { + bundleExtensionsIntoJar, + type BuildContextLike as BuildContextLike_bundleExtensionsIntoJar +} from "./bundleExtensionsIntoJar"; -export type BuildContextLike = BuildContextLike_generatePom & { - keycloakifyBuildDirPath: string; - themeNames: string[]; - artifactId: string; - themeVersion: string; - cacheDirPath: string; - implementedThemeTypes: BuildContext["implementedThemeTypes"]; -}; +export type BuildContextLike = BuildContextLike_generatePom & + BuildContextLike_bundleExtensionsIntoJar & { + keycloakifyBuildDirPath: string; + themeNames: string[]; + artifactId: string; + themeVersion: string; + cacheDirPath: string; + implementedThemeTypes: BuildContext["implementedThemeTypes"]; + }; assert(); @@ -234,12 +239,19 @@ export async function buildJar(params: { ) ); + const jarFilePath_generatedByMaven = pathJoin( + keycloakifyBuildCacheDirPath, + "target", + `${buildContext.artifactId}-${buildContext.themeVersion}.jar` + ); + + await bundleExtensionsIntoJar({ + buildContext, + jarFilePath: jarFilePath_generatedByMaven + }); + await fs.rename( - pathJoin( - keycloakifyBuildCacheDirPath, - "target", - `${buildContext.artifactId}-${buildContext.themeVersion}.jar` - ), + jarFilePath_generatedByMaven, pathJoin(buildContext.keycloakifyBuildDirPath, jarFileBasename) ); } diff --git a/src/bin/keycloakify/buildJars/bundleExtensionsIntoJar.ts b/src/bin/keycloakify/buildJars/bundleExtensionsIntoJar.ts new file mode 100644 index 00000000..bad85b42 --- /dev/null +++ b/src/bin/keycloakify/buildJars/bundleExtensionsIntoJar.ts @@ -0,0 +1,137 @@ +import { downloadAndExtractArchive } from "../../tools/downloadAndExtractArchive"; +import { assert } from "tsafe/assert"; +import type { BuildContext } from "../../shared/buildContext"; +import { transformCodebase } from "../../tools/transformCodebase"; +import { join as pathJoin, basename as pathBasename, sep as pathSep } from "path"; +import { rm } from "../../tools/fs.rm"; +import { extractArchive } from "../../tools/extractArchive"; +import * as crypto from "crypto"; + +export type BuildContextLike = { + cacheDirPath: string; + fetchOptions: BuildContext["fetchOptions"]; + extensionJars: BuildContext["extensionJars"]; +}; + +assert(); + +export async function bundleExtensionsIntoJar(params: { + jarFilePath: string; + buildContext: BuildContextLike; +}): Promise { + const { jarFilePath, buildContext } = params; + + if (buildContext.extensionJars.length === 0) { + return; + } + + const mergeDirPath = pathJoin( + buildContext.cacheDirPath, + `merge_${pathBasename(jarFilePath).replace(/\.jar$/, "")}_${crypto + .createHash("sha256") + .update(jarFilePath) + .digest("hex") + .substring(0, 5)}` + ); + + await extractArchive({ + archiveFilePath: jarFilePath, + onArchiveFile: async ({ relativeFilePathInArchive, writeFile }) => + writeFile({ + filePath: pathJoin(mergeDirPath, relativeFilePathInArchive) + }) + }); + + for (const extensionJar of buildContext.extensionJars) { + const transformSourceCode = (params: { + fileRelativePath: string; + sourceCode: Buffer; + }): { modifiedSourceCode: Buffer } | undefined => { + const { fileRelativePath } = params; + + if (!fileRelativePath.startsWith(`META-INF${pathSep}`)) { + for (const ext of [".DSA", ".SF", ".RSA"]) { + if (fileRelativePath.endsWith(ext)) { + return undefined; + } + } + } + + return undefined; + }; + + switch (extensionJar.type) { + case "path": + await extractArchive({ + archiveFilePath: extensionJar.path, + onArchiveFile: async ({ + relativeFilePathInArchive, + writeFile, + readFile + }) => { + const transformResult = transformSourceCode({ + fileRelativePath: relativeFilePathInArchive, + sourceCode: await readFile() + }); + + if (transformResult === undefined) { + return; + } + + await writeFile({ + filePath: pathJoin(mergeDirPath, relativeFilePathInArchive), + modifiedData: transformResult.modifiedSourceCode + }); + } + }); + + break; + + case "url": { + const { extractedDirPath } = await downloadAndExtractArchive({ + url: extensionJar.url, + cacheDirPath: buildContext.cacheDirPath, + fetchOptions: buildContext.fetchOptions, + uniqueIdOfOnArchiveFile: "noOp", + onArchiveFile: async ({ fileRelativePath, writeFile }) => + writeFile({ fileRelativePath }) + }); + transformCodebase({ + srcDirPath: extractedDirPath, + destDirPath: mergeDirPath, + transformSourceCode + }); + break; + } + } + + /* + transformCodebase({ + srcDirPath: extractedDirPath, + destDirPath: mergeDirPath, + transformSourceCode: ({ fileRelativePath, sourceCode }) => { + if (fileRelativePath === pathJoin("META-INF", "MANIFEST.MF")) { + const sourceCodeStr = sourceCode.toString("utf8"); + + const lines = sourceCodeStr.split(/\r?\n/); + + console.log(lines); + + return { + modifiedSourceCode: Buffer.concat([ + sourceCode, + Buffer.from( + `Class-Path: ${pathBasename(userProvidedJarFilePathOrUrl)}\n` + ) + ]) + }; + } + } + }); + */ + } + + // TODO: Acctually build new jar + + await rm(mergeDirPath, { recursive: true, force: true }); +} diff --git a/src/bin/keycloakify/generateResources/generateResourcesForMainTheme.ts b/src/bin/keycloakify/generateResources/generateResourcesForMainTheme.ts index 41fe8ddd..382631aa 100644 --- a/src/bin/keycloakify/generateResources/generateResourcesForMainTheme.ts +++ b/src/bin/keycloakify/generateResources/generateResourcesForMainTheme.ts @@ -314,8 +314,7 @@ export async function generateResourcesForMainTheme(params: { } const { extractedDirPath } = await downloadAndExtractArchive({ - urlOrPath: - "https://repo1.maven.org/maven2/org/keycloak/keycloak-account-ui/25.0.1/keycloak-account-ui-25.0.1.jar", + url: "https://repo1.maven.org/maven2/org/keycloak/keycloak-account-ui/25.0.1/keycloak-account-ui-25.0.1.jar", cacheDirPath: buildContext.cacheDirPath, fetchOptions: buildContext.fetchOptions, uniqueIdOfOnArchiveFile: "bring_in_account_v3_i18n_messages", diff --git a/src/bin/shared/buildContext.ts b/src/bin/shared/buildContext.ts index b0c5439d..38a91f96 100644 --- a/src/bin/shared/buildContext.ts +++ b/src/bin/shared/buildContext.ts @@ -26,6 +26,8 @@ import { type ThemeType } from "./constants"; import { id } from "tsafe/id"; import chalk from "chalk"; import { getProxyFetchOptions, type ProxyFetchOptions } from "../tools/fetchProxyOptions"; +import { removeDuplicates } from "evt/tools/reducers/removeDuplicates"; +import { same } from "evt/tools/inDepth/same"; export type BuildContext = { themeVersion: string; @@ -61,6 +63,7 @@ export type BuildContext = { keycloakVersionRange: KeycloakVersionRange; jarFileBasename: string; }[]; + extensionJars: ({ type: "path"; path: string } | { type: "url"; url: string })[]; startKeycloakOptions: { dockerImage: | { @@ -88,6 +91,7 @@ export type BuildOptions = { loginThemeResourcesFromKeycloakVersion?: string; keycloakifyBuildDirPath?: string; kcContextExclusionsFtl?: string; + extensionJars?: string[]; startKeycloakOptions?: { dockerImage?: string; dockerExtraArgs?: string[]; @@ -360,6 +364,7 @@ export function getBuildContext(params: { loginThemeResourcesFromKeycloakVersion: z.string().optional(), keycloakifyBuildDirPath: z.string().optional(), kcContextExclusionsFtl: z.string().optional(), + extensionJars: z.array(z.string()).optional(), startKeycloakOptions: zStartKeycloakOptions.optional() }), zAccountThemeImplAndKeycloakVersionTargets @@ -520,6 +525,36 @@ export function getBuildContext(params: { return pathJoin(projectDirPath, resolvedViteConfig.buildDir); })(); + const buildForKeycloakMajorVersionNumber = (() => { + const envValue = process.env[BUILD_FOR_KEYCLOAK_MAJOR_VERSION_ENV_NAME]; + + if (envValue === undefined) { + return undefined; + } + + const major = parseInt(envValue); + + assert(!isNaN(major)); + + return major; + })(); + + function urlOrPathToDiscriminatingWrapper( + urlOrPath: string + ): { type: "url"; url: string } | { type: "path"; path: string } { + if (/^https?:\/\//.test(urlOrPath)) { + return { type: "url", url: urlOrPath }; + } + + return { + type: "path", + path: getAbsoluteAndInOsFormatPath({ + pathIsh: urlOrPath, + cwd: projectDirPath + }) + }; + } + return { bundler, packageJsonFilePath, @@ -717,21 +752,6 @@ export function getBuildContext(params: { `keycloak-theme-for-kc-${range}.jar`; build_for_specific_keycloak_major_version: { - const buildForKeycloakMajorVersionNumber = (() => { - const envValue = - process.env[BUILD_FOR_KEYCLOAK_MAJOR_VERSION_ENV_NAME]; - - if (envValue === undefined) { - return undefined; - } - - const major = parseInt(envValue); - - assert(!isNaN(major)); - - return major; - })(); - if (buildForKeycloakMajorVersionNumber === undefined) { break build_for_specific_keycloak_major_version; } @@ -931,6 +951,10 @@ export function getBuildContext(params: { return jarTargets; })(), + extensionJars: (buildForKeycloakMajorVersionNumber !== undefined + ? [] + : buildOptions.extensionJars ?? [] + ).map(urlOrPath => urlOrPathToDiscriminatingWrapper(urlOrPath)), startKeycloakOptions: { dockerImage: (() => { if (buildOptions.startKeycloakOptions?.dockerImage === undefined) { @@ -949,21 +973,14 @@ export function getBuildContext(params: { })(), dockerExtraArgs: buildOptions.startKeycloakOptions?.dockerExtraArgs ?? [], keycloakExtraArgs: buildOptions.startKeycloakOptions?.keycloakExtraArgs ?? [], - extensionJars: (buildOptions.startKeycloakOptions?.extensionJars ?? []).map( - urlOrPath => { - if (/^https?:\/\//.test(urlOrPath)) { - return { type: "url", url: urlOrPath }; - } - - return { - type: "path", - path: getAbsoluteAndInOsFormatPath({ - pathIsh: urlOrPath, - cwd: projectDirPath - }) - }; - } - ), + extensionJars: [ + ...(buildForKeycloakMajorVersionNumber !== undefined + ? buildOptions.extensionJars ?? [] + : []), + ...(buildOptions.startKeycloakOptions?.extensionJars ?? []) + ] + .map(urlOrPath => urlOrPathToDiscriminatingWrapper(urlOrPath)) + .reduce(...removeDuplicates(same)), realmJsonFilePath: buildOptions.startKeycloakOptions?.realmJsonFilePath === undefined ? undefined diff --git a/src/bin/shared/downloadKeycloakDefaultTheme.ts b/src/bin/shared/downloadKeycloakDefaultTheme.ts index c1b87979..c72e251a 100644 --- a/src/bin/shared/downloadKeycloakDefaultTheme.ts +++ b/src/bin/shared/downloadKeycloakDefaultTheme.ts @@ -21,7 +21,7 @@ export async function downloadKeycloakDefaultTheme(params: { let kcNodeModulesKeepFilePaths_lastAccountV1: Set | undefined = undefined; const { extractedDirPath } = await downloadAndExtractArchive({ - urlOrPath: `https://repo1.maven.org/maven2/org/keycloak/keycloak-themes/${keycloakVersion}/keycloak-themes-${keycloakVersion}.jar`, + url: `https://repo1.maven.org/maven2/org/keycloak/keycloak-themes/${keycloakVersion}/keycloak-themes-${keycloakVersion}.jar`, cacheDirPath: buildContext.cacheDirPath, fetchOptions: buildContext.fetchOptions, uniqueIdOfOnArchiveFile: "downloadKeycloakDefaultTheme", diff --git a/src/bin/start-keycloak/start-keycloak.ts b/src/bin/start-keycloak/start-keycloak.ts index 206f6f5e..0686be85 100644 --- a/src/bin/start-keycloak/start-keycloak.ts +++ b/src/bin/start-keycloak/start-keycloak.ts @@ -200,7 +200,7 @@ export async function command(params: { cliCommandOptions: CliCommandOptions }) const { archiveFilePath } = await downloadAndExtractArchive({ cacheDirPath: buildContext.cacheDirPath, fetchOptions: buildContext.fetchOptions, - urlOrPath: extensionJar.url, + url: extensionJar.url, uniqueIdOfOnArchiveFile: "no extraction", onArchiveFile: async () => {} }); diff --git a/src/bin/tools/downloadAndExtractArchive.ts b/src/bin/tools/downloadAndExtractArchive.ts index 3a1e1f00..75749d74 100644 --- a/src/bin/tools/downloadAndExtractArchive.ts +++ b/src/bin/tools/downloadAndExtractArchive.ts @@ -1,15 +1,14 @@ import fetch, { type FetchOptions } from "make-fetch-happen"; import { mkdir, unlink, writeFile, readdir, readFile } from "fs/promises"; -import { dirname as pathDirname, join as pathJoin, basename as pathBasename } from "path"; +import { dirname as pathDirname, join as pathJoin } from "path"; import { assert } from "tsafe/assert"; import { extractArchive } from "./extractArchive"; import { existsAsync } from "./fs.existsAsync"; import * as crypto from "crypto"; import { rm } from "./fs.rm"; -import * as fsPr from "fs/promises"; export async function downloadAndExtractArchive(params: { - urlOrPath: string; + url: string; uniqueIdOfOnArchiveFile: string; onArchiveFile: (params: { fileRelativePath: string; @@ -22,33 +21,16 @@ export async function downloadAndExtractArchive(params: { cacheDirPath: string; fetchOptions: FetchOptions | undefined; }): Promise<{ extractedDirPath: string; archiveFilePath: string }> { - const { - urlOrPath, - uniqueIdOfOnArchiveFile, - onArchiveFile, - cacheDirPath, - fetchOptions - } = params; + const { url, uniqueIdOfOnArchiveFile, onArchiveFile, cacheDirPath, fetchOptions } = + params; - const isUrl = /^https?:\/\//.test(urlOrPath); - - const archiveFileBasename = isUrl - ? urlOrPath.split("?")[0].split("/").reverse()[0] - : pathBasename(urlOrPath); + const archiveFileBasename = url.split("?")[0].split("/").reverse()[0]; const archiveFilePath = pathJoin(cacheDirPath, archiveFileBasename); download: { await mkdir(pathDirname(archiveFilePath), { recursive: true }); - if (!isUrl) { - await fsPr.copyFile(urlOrPath, archiveFilePath); - - break download; - } - - const url = urlOrPath; - if (await existsAsync(archiveFilePath)) { const isDownloaded = await SuccessTracker.getIsDownloaded({ cacheDirPath,