diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 2386046..6665967 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -68,5 +68,6 @@ jobs: - uses: ./ with: local-packages: test/local-packages.json + cache-local-packages: true - run: typst --version - run: typst compile test/local-packages.typ diff --git a/README.md b/README.md index 3387b9e..ace61d2 100644 --- a/README.md +++ b/README.md @@ -7,8 +7,8 @@ This action provides the following functionality for GitHub Actions users: - **Installing** a version of Typst and adding it to the PATH -- **Caching** [packages](https://github.com/typst/packages) dependencies -- **Downloading** ZIP archive as local packages +- **Caching** [packages] dependencies +- **Downloading** ZIP archives as local packages ```yaml - uses: typst-community/setup-typst@v4 @@ -70,17 +70,19 @@ jobs: #### ZIP archive packages download -**`local-packages`:** Used to specify the path to a JSON file containing names and ZIP archive URLs of packages as local packages under the `local` key. +- **`local-packages`:** Used to specify the path to a JSON file containing names and ZIP archive URLs of packages as local packages under the `local` key. +- **`cache-local-packages`:** When `true`, local packages set by `local-packages` will be cached independently of `@preview` packages. ```yaml # Example workflow YAML file - uses: typst-community/setup-typst@v4 with: local-packages: packages.json + cache-local-packages: true ``` ```js -// Example JSON file (packages.js) +// Example JSON file (packages.json) { "local": { "glossarium": "https://github.com/typst-community/glossarium/archive/refs/tags/v0.5.4.zip", @@ -89,6 +91,10 @@ jobs: } ``` +> [!NOTE] +> - For links to download GitHub repositories, please refer to [_Downloading source code archives_]. +> - The namespace for local packages is `local`. The SemVer versions of local packages are read from its `typst.toml`. + #### Token **`token`:** The token used to authenticate when fetching Typst distributions from [typst/typst]. When running this action on github.com, the default value is sufficient. When running on GHES, you can pass a personal access token for github.com if you are experiencing rate limiting. @@ -102,6 +108,8 @@ jobs: #### Uploading workflow artifact +If you require storing and sharing data from a workflow, you can use [artifacts]. + ```yaml - uses: typst-community/setup-typst@v4 - run: typst compile paper.typ paper.pdf @@ -111,9 +119,9 @@ jobs: path: paper.pdf ``` -#### Expanding font support with Fontist +#### Installing fonts with Fontist -If your tasks require extending beyond the set of fonts in GitHub Actions runner, you can employ the Fontist to facilitate custom font installations. Here's an example showcasing how to use [fontist/setup-fontist] to add new fonts: +If you require installing fonts in GitHub Actions runner, you can use [Fontist]. ```yaml - uses: fontist/setup-fontist@v2 @@ -124,4 +132,7 @@ If your tasks require extending beyond the set of fonts in GitHub Actions runner [Typst]: https://typst.app/ [typst/typst]: https://github.com/typst/typst -[fontist/setup-fontist]: https://github.com/fontist/setup-fontist +[packages]: https://github.com/typst/packages +[_Downloading source code archives_]: https://docs.github.com/en/repositories/working-with-files/using-files/downloading-source-code-archives +[artifacts]: https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/storing-and-sharing-data-from-a-workflow +[Fontist]: https://www.fontist.org/ diff --git a/README_zh-Hans-CN.md b/README_zh-Hans-CN.md index ed578c9..3bff7a6 100644 --- a/README_zh-Hans-CN.md +++ b/README_zh-Hans-CN.md @@ -7,7 +7,7 @@ 此操作为 GitHub Actions 用户提供以下功能: - **安装**指定版本的 Typst -- **缓存**依赖的[包](https://github.com/typst/packages) +- **缓存**依赖的[包] - **下载** ZIP 压缩文件作为本地包 ```yaml @@ -70,17 +70,19 @@ jobs: #### ZIP 压缩文件作为包下载 -**`local-packages`:** 指向一个在 `local` 键下有包名称与对应 ZIP 压缩文件 URL 的 JSON 文件。 +- **`local-packages`:** 指向一个在 `local` 键下有包名称与对应 ZIP 压缩文件 URL 的 JSON 文件。 +- **`cache-local-packages`:** 当设置为 `true` 时,在 `local-packages` 中设定的包将被缓存(缓存独立于 `@preview` 包)。 ```yaml # Example workflow YAML file - uses: typst-community/setup-typst@v4 with: local-packages: packages.json + cache-local-packages: true ``` ```js -// Example JSON file (packages.js) +// Example JSON file (packages.json) { "local": { "glossarium": "https://github.com/typst-community/glossarium/archive/refs/tags/v0.5.4.zip", @@ -89,6 +91,10 @@ jobs: } ``` +> [!NOTE] +> - 对于下载 GitHub 存储库需要的链接,请参阅 [《下载源代码存档》]。 +> - 本地包的命名空间为 `local`,SemVer 版本号从 `typst.toml` 读取。 + #### 令牌 **`token`:** 当从 [typst/typst] 拉取版本时使用的 GitHub 令牌。当在 github.com 上运行操作时,使用默认值;当在 GitHub Enterprise Server(GHES)上运行,可以传递一个 github.com 的个人访问令牌规避速率限制问题。 @@ -100,7 +106,9 @@ jobs: ### 自定义组合 -#### 上传到工作流工件 +#### 上传到工作流构件 + +如果需要从工作流存储和共享数据,可以使用[构件]。 ```yaml - uses: typst-community/setup-typst@v4 @@ -111,9 +119,9 @@ jobs: path: paper.pdf ``` -#### 使用 Fontist 拓展字体支持 +#### 使用 Fontist 安装字体 -如需为 GitHub Actions 运行器拓展字体库,可使用 Fontist 进行自定义字体安装。以下是使用 [fontist/setup-fontist] 添加新字体的范例: +如果需要在 GitHub Actions 运行器中安装字体,可以使用 [Fontist]。 ```yaml - uses: fontist/setup-fontist@v2 @@ -124,4 +132,7 @@ jobs: [Typst]: https://typst.app/ [typst/typst]: https://github.com/typst/typst -[fontist/setup-fontist]: https://github.com/fontist/setup-fontist +[包]: https://github.com/typst/packages +[《下载源代码存档》]: https://docs.github.com/zh/repositories/working-with-files/using-files/downloading-source-code-archives +[构件]: https://docs.github.com/zh/actions/writing-workflows/choosing-what-your-workflow-does/storing-and-sharing-data-from-a-workflow#comparing-artifacts-and-dependency-caching +[Fontist]: https://www.fontist.org/ diff --git a/action.yml b/action.yml index e109755..d3ff507 100644 --- a/action.yml +++ b/action.yml @@ -18,6 +18,9 @@ inputs: local-packages: description: "Used to specify the path to a JSON file containing names and ZIP archive URLs of packages as local packages under the 'local' key." required: false + cache-local-packages: + description: "When 'true', local packages set by 'local-packages' will be cached independently of '@preview' packages." + default: false token: description: "The token used to authenticate when fetching Typst distributions from typst/typst. When running this action on github.com, the default value is sufficient. When running on GHES, you can pass a personal access token for github.com if you are experiencing rate limiting." default: ${{ github.server_url == 'https://github.com' && github.token || '' }} diff --git a/package-lock.json b/package-lock.json index 629c1b1..1ef44a2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4,7 +4,6 @@ "requires": true, "packages": { "": { - "name": "setup-typst", "dependencies": { "@actions/cache": "^4.0.0", "@actions/core": "^1.10.1", diff --git a/src/main.ts b/src/main.ts index 660125e..e1214ac 100644 --- a/src/main.ts +++ b/src/main.ts @@ -3,7 +3,7 @@ import * as cache from "@actions/cache"; import * as core from "@actions/core"; import * as exec from "@actions/exec"; import * as github from "@actions/github"; -import * as glob from "@actions/glob"; +import { hashFiles } from "@actions/glob"; import * as tc from "@actions/tool-cache"; import fs from "fs"; import path from "path"; @@ -19,33 +19,41 @@ async function move(src: string, dest: string) { fs.cpSync(src, dest, { recursive: true, force: true }); fs.rmSync(src, { recursive: true, force: true }); } catch (error) { - core.warning(`Failed to move '${src}' to '${dest}': ${(error as Error).message}.`); + core.warning( + `Failed to move '${src}' to '${dest}': ${(error as Error).message}.` + ); } } } -async function getReleases( +async function listReleases( octokit: any, repoSet: { owner: string; repo: string } ) { - core.info(`Fetching releases for repository '${repoSet.owner}/${repoSet.repo}'.`); + core.info( + `Fetching releases list for repository ${repoSet.owner}/${repoSet.repo}.` + ); if (octokit) { return await octokit.paginate(octokit.rest.repos.listReleases, repoSet); } else { const releasesUrl = `https://api.github.com/repos/${repoSet.owner}/${repoSet.repo}/releases`; - core.debug(`Fetching releases from '${releasesUrl}' without authentication.`); + core.debug( + `Fetching releases list from ${releasesUrl} without authentication.` + ); const releasesResponse = await tc.downloadTool(releasesUrl); try { - core.info(`Successfully downloaded releases from '${releasesUrl}'.`); + core.debug(`Successfully downloaded releases list from ${releasesUrl}.`); return JSON.parse(fs.readFileSync(releasesResponse, "utf8")); } catch (error) { - core.setFailed(`Failed to parse releases from '${releasesUrl}': ${(error as Error).message}. This may be caused by API rate limit exceeded.`); + core.setFailed( + `Failed to parse releases from ${releasesUrl}: ${(error as Error).message}. This may be caused by API rate limit exceeded.` + ); process.exit(1); } } } -async function getVersion( +async function getVersionExact( releases: any[], version: string, allowPrereleases: boolean @@ -53,35 +61,49 @@ async function getVersion( const versions = releases .map((release) => release.tag_name.slice(1)) .filter((v) => semver.valid(v)); - const resolvedVersion = semver.maxSatisfying(versions, version === "latest" ? "*" : version, { - includePrerelease: allowPrereleases, - }); + const resolvedVersion = semver.maxSatisfying( + versions, + version === "latest" ? "*" : version, + { + includePrerelease: allowPrereleases, + } + ); if (resolvedVersion) { - core.info(`Resolved version: '${resolvedVersion}'.`); + core.info(`Resolved Typst version: ${resolvedVersion}.`); } else { - core.warning(`No matching version found for input: '${version}'.`); + core.setFailed(`Typst ${version} could not be resolved.`); + process.exit(1); } return resolvedVersion; } async function downloadAndCacheTypst(version: string) { - core.info(`Downloading and caching Typst version: '${version}'.`); - const target = { - "darwin,arm64": "aarch64-apple-darwin", - "linux,x64": "x86_64-unknown-linux-musl", - "linux,arm": "armv7-unknown-linux-musleabi", - "darwin,x64": "x86_64-apple-darwin", - "win32,x64": "x86_64-pc-windows-msvc", - "linux,arm64": "aarch64-unknown-linux-musl", - }[[process.platform, process.arch].join(",")]; - const archiveExt = { - darwin: ".tar.xz", - linux: ".tar.xz", - win32: ".zip", - }[process.platform.toString()]!; + core.info(`Downloading and caching Typst ${version}.`); + let target, archiveExt; + if (semver.gte(version, "0.3.0") || process.platform == "win32") { + target = { + "darwin,arm64": "aarch64-apple-darwin", + "linux,x64": "x86_64-unknown-linux-musl", + "linux,arm": "armv7-unknown-linux-musleabi", + "darwin,x64": "x86_64-apple-darwin", + "win32,x64": "x86_64-pc-windows-msvc", + "linux,arm64": "aarch64-unknown-linux-musl", + }[[process.platform, process.arch].join(",")]; + archiveExt = { + darwin: ".tar.xz", + linux: ".tar.xz", + win32: ".zip", + }[process.platform.toString()]!; + } else { + target = { + darwin: "x86_64-apple-darwin", + linux: "x86_64-unknown-linux-gnu" + }[process.platform.toString()]!; + archiveExt = ".tar.gz"; + } const folder = `typst-${target}`; const file = `${folder}${archiveExt}`; - core.debug(`Determined target: '${target}', archive extension: '${archiveExt}'.`); + core.debug(`Determined target: ${target}, archive extension: ${archiveExt}.`); let found = await tc.downloadTool( `https://github.com/typst/typst/releases/download/v${version}/${file}` ); @@ -95,8 +117,8 @@ async function downloadAndCacheTypst(version: string) { } found = await tc.extractZip(found); } else { - found = await tc.extractTar(found, undefined, "xJ"); - core.debug(`Extracted archive for Typst version: '${version}'.`); + found = await tc.extractTar(found, undefined, semver.gte(version, "0.3.0") ? "xJ" : "xz"); + core.debug(`Extracted archive for Typst version ${version}.`); } found = path.join(found, folder); found = await tc.cacheDir(found, "typst", version); @@ -111,34 +133,36 @@ const TYPST_PACKAGES_DIR = { (os.homedir() ? path.join(os.homedir(), ".cache") : undefined)!, "typst/packages" ), - darwin: () => path.join(process.env.HOME!, "Library/Caches", "typst/packages"), + darwin: () => + path.join(process.env.HOME!, "Library/Caches", "typst/packages"), win32: () => path.join(process.env.LOCALAPPDATA!, "typst/packages"), }[process.platform as string]!(); async function cachePackages(cachePackage: string) { - core.debug(`Checking if dependency path exists: '${cachePackage}'.`); - if (fs.existsSync(cachePackage)) { - const cacheDir = TYPST_PACKAGES_DIR + "/preview"; - const hash = await glob.hashFiles(cachePackage); - const primaryKey = `typst-preview-packages-${hash}`; - core.info(`Computed cache key: '${primaryKey}'.`); - const cacheKey = await cache.restoreCache([cacheDir], primaryKey); - if (cacheKey != undefined) { - core.info(`✅ Packages restored from cache.`); - } else { - core.info(`Cache miss. Compiling Typst packages.`); - await exec.exec(`typst compile ${cachePackage}`); - try { - let cacheId = await cache.saveCache([cacheDir], primaryKey); - core.info(`✅ Cache saved successfully with key: '${primaryKey}'.`); - core.debug(`Cache ID: ${cacheId}`); - } catch (error) { - core.warning(`Failed to save cache: ${(error as Error).message}.`); - return; - } - } + if (!fs.existsSync(cachePackage)) { + core.warning( + `Dependency path '${cachePackage}' not found. Skipping caching.` + ); + return; + } + const cacheDir = TYPST_PACKAGES_DIR + "/preview"; + const hash = await hashFiles(cachePackage); + const primaryKey = `typst-preview-packages-${hash}`; + core.info(`Computed cache key: ${primaryKey}.`); + const cacheKey = await cache.restoreCache([cacheDir], primaryKey); + if (cacheKey != undefined) { + core.info(`✅ Packages restored from cache.`); } else { - core.warning(`Dependency path '${cachePackage}' not found. Skipping caching.`); + core.debug(`Cache miss. Compiling Typst packages.`); + await exec.exec(`typst compile ${cachePackage}`); + try { + let cacheId = await cache.saveCache([cacheDir], primaryKey); + core.info(`✅ Cache saved successfully with key: ${primaryKey}.`); + core.debug(`Cache ID: ${cacheId}`); + } catch (error) { + core.warning(`Failed to save cache: ${(error as Error).message}.`); + } + return; } } @@ -149,7 +173,9 @@ function getPackageVersion(toml: string): string { content = fs.readFileSync(toml, "utf-8"); core.info(`Successfully read TOML file: '${toml}'.`); } catch (error) { - core.warning(`Failed to read TOML file '${toml}': ${(error as Error).message}. Defaulting to version '0.0.0'.`); + core.warning( + `Failed to read TOML file '${toml}': ${(error as Error).message}. Defaulting to version '0.0.0'.` + ); return "0.0.0"; } const lines = content.split(/\r?\n/); @@ -171,66 +197,118 @@ function getPackageVersion(toml: string): string { return "0.0.0"; } -async function downloadLocalPackages(packages: { - local: { [key: string]: string }; -}) { +const packagesDir = path.join(TYPST_PACKAGES_DIR, "/local"); + +async function downloadLocalPackage(name: string, url: string) { + const packageDir = path.join(packagesDir, name); + if (!fs.existsSync(packageDir)) { + fs.mkdirSync(packageDir); + core.debug(`Created directory '${packageDir}' for package ${name}.`); + } else { + core.warning( + `Directory '${packageDir}' already exists. Check for duplicate package names.` + ); + } + core.info(`Downloading package ${name} from ${url}.`); + let packageResponse = await tc.downloadTool(url); + if (process.platform == "win32") { + if (!packageResponse.endsWith(".zip")) { + fs.renameSync( + packageResponse, + path.join( + path.dirname(packageResponse), + `${path.basename(packageResponse)}.zip` + ) + ); + packageResponse = path.join( + path.dirname(packageResponse), + `${path.basename(packageResponse)}.zip` + ); + } + } + packageResponse = await tc.extractZip(packageResponse); + core.debug(`Extracted package ${name}.`); + const dirContent = await new Promise((resolve, reject) => { + fs.readdir(packageResponse, (err, files) => { + if (err) reject(err); + else resolve(files); + }); + }); + if (dirContent.length === 1) { + const innerPath = path.join(packageResponse, dirContent[0]); + const stats = fs.statSync(innerPath); + if (stats.isDirectory()) { + const packageVersion = getPackageVersion( + path.join(innerPath, "typst.toml") + ); + move(innerPath, path.join(packageDir, packageVersion)); + } + } else { + const packageVersion = getPackageVersion( + path.join(packageResponse, "typst.toml") + ); + move(packageResponse, path.join(packageDir, packageVersion)); + } + core.info(`✅ Downloaded ${name} to '${packageDir}'`); +} + +async function downloadAndCacheLocalPackages( + localPackage: string, + cacheLocalPackages: boolean +) { + if (!fs.existsSync(localPackage)) { + core.warning( + `Local packages path '${localPackage}' not found. Skipping downloading.` + ); + return; + } + if (cacheLocalPackages) { + const hash = await hashFiles(localPackage); + const primaryKey = `typst-local-packages-${hash}`; + core.info(`Computed cache key: ${primaryKey}.`); + const cacheKey = await cache.restoreCache([packagesDir], primaryKey); + if (cacheKey != undefined) { + core.info(`✅ Local packages restored from cache.`); + return; + } + core.debug(`Cache miss. Downloading local packages.`); + } + let packages; + try { + packages = JSON.parse(fs.readFileSync(localPackage, "utf8")); + } catch (error) { + core.warning( + `Failed to parse local-packages json file: ${(error as Error).message}. Skipping downloading.` + ); + return; + } core.info(`Downloading local packages.`); - const packagesDir = TYPST_PACKAGES_DIR + "/local"; if (!fs.existsSync(packagesDir)) { fs.mkdirSync(packagesDir, { recursive: true }); core.debug(`Created local packages directory: '${packagesDir}'.`); } await Promise.all( - Object.entries(packages.local).map(async ([key, value]) => { - core.info(`Downloading package: '${key}' from '${value}'.`); - const packageDir = path.join(packagesDir, key); - if (!fs.existsSync(packageDir)) { - fs.mkdirSync(packageDir); - core.debug(`Created directory for package: '${packageDir}'.`); - } else { - core.warning(`Directory '${packageDir}' already exists. Check for duplicate package names.`); - } - let packageResponse = await tc.downloadTool(value); - core.info(`Downloaded package: '${key}'.`); - if (process.platform == "win32") { - if (!packageResponse.endsWith(".zip")) { - fs.renameSync( - packageResponse, - path.join( - path.dirname(packageResponse), - `${path.basename(packageResponse)}.zip` - ) - ); - packageResponse = path.join( - path.dirname(packageResponse), - `${path.basename(packageResponse)}.zip` - ); - } - } - packageResponse = await tc.extractZip(packageResponse); - core.info(`Extracted package: '${key}'.`); - const dirContent = await new Promise((resolve, reject) => { - fs.readdir(packageResponse, (err, files) => { - if (err) reject(err); - else resolve(files); - }); - }); - if (dirContent.length === 1) { - const innerPath = path.join(packageResponse, dirContent[0]); - const stats = fs.statSync(innerPath); - if (stats.isDirectory()) { - const packageVersion = getPackageVersion(path.join(innerPath, "typst.toml")); - move(innerPath, path.join(packageDir, packageVersion)); - } + Object.entries(packages.local).map(([key, value]) => { + if (typeof value === "string") { + return downloadLocalPackage(key, value); } else { - const packageVersion = getPackageVersion( - path.join(packageResponse, "typst.toml") - ); - move(packageResponse, path.join(packageDir, packageVersion)); + core.warning(`Invalid package URL for ${key}: Expected a string.`); + return Promise.resolve(); } - core.info(`Downloaded ${key} to ${packageDir}`); }) ); + if (cacheLocalPackages) { + try { + const hash = await hashFiles(localPackage); + const primaryKey = `typst-local-packages-${hash}`; + let cacheId = await cache.saveCache([packagesDir], primaryKey); + core.info(`✅ Cache saved successfully with key: ${primaryKey}.`); + core.debug(`Cache ID: ${cacheId}`); + } catch (error) { + core.warning(`Failed to save cache: ${(error as Error).message}.`); + } + } + return; } const token = core.getInput("token"); @@ -241,15 +319,10 @@ const repoSet = { owner: "typst", repo: "typst", }; -const releases = await getReleases(octokit, repoSet); -const allowPrereleases = core.getBooleanInput("allow-prereleases"); +const releases = await listReleases(octokit, repoSet); const version = core.getInput("typst-version"); -const versionExact = await getVersion(releases, version, allowPrereleases); -if (!versionExact) { - core.setFailed(`Typst ${version} could not be resolved.`); - process.exit(1); -} -core.debug(`Resolved version: v${versionExact}`); +const allowPrereleases = core.getBooleanInput("allow-prereleases"); +const versionExact = await getVersionExact(releases, version, allowPrereleases); let found = tc.find("typst", versionExact); core.setOutput("cache-hit", !!found); if (!found) { @@ -263,14 +336,7 @@ if (cachePackage) { await cachePackages(cachePackage); } const localPackage = core.getInput("local-packages"); +const cacheLocalPackages = core.getBooleanInput("cache-local-packages"); if (localPackage) { - let localPackages; - try { - localPackages = JSON.parse(fs.readFileSync(localPackage, "utf8")); - } catch (error) { - core.warning( - `Failed to parse local-packages json file: ${(error as Error).message}. Packages will not be downloaded.` - ); - } - await downloadLocalPackages(localPackages); + await downloadAndCacheLocalPackages(localPackage, cacheLocalPackages); }