diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 283add8..a22f448 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -47,7 +47,7 @@ jobs: run: | set -euo pipefail latest_version="$(jq -r '.version' package.json)" - count_expected=21 + count_expected=23 count_actual="$(grep -c "setup-pixi@v$latest_version" README.md || true)" if [ "$count_actual" -ne "$count_expected" ]; then echo "::error file=README.md::Expected $count_expected mentions of \`setup-pixi@v$latest_version\` in README.md, but found $count_actual." diff --git a/README.md b/README.md index e5a1bb2..33277de 100644 --- a/README.md +++ b/README.md @@ -418,6 +418,27 @@ This can be overwritten by setting the `manifest-path` input argument. manifest-path: pyproject.toml ``` +### Working directory for monorepos + +If you're working with a monorepo where your pixi project is in a subdirectory, you can use the `working-directory` input to specify where pixi should look for manifest files (`pixi.toml` or `pyproject.toml`). + +```yml +- uses: prefix-dev/setup-pixi@v0.9.3 + with: + working-directory: ./packages/my-project +``` + +This will make pixi look for `pixi.toml` or `pyproject.toml` in the `./packages/my-project` directory instead of the repository root. All pixi commands will be executed from this working directory. + +You can combine `working-directory` with `manifest-path` if needed: + +```yml +- uses: prefix-dev/setup-pixi@v0.9.3 + with: + working-directory: ./packages/my-project + manifest-path: custom-pixi.toml +``` + ### Only install pixi If you only want to install pixi and not install the current project, you can use the `run-install` option. diff --git a/action.yml b/action.yml index f691688..fcdf2fa 100644 --- a/action.yml +++ b/action.yml @@ -18,6 +18,10 @@ inputs: One of `q`, `default`, `v`, `vv`, or `vvv`. manifest-path: description: Path to the manifest file (i.e., `pixi.toml`) to use for the pixi CLI. Defaults to `pixi.toml`. + working-directory: + description: | + Working directory to use for pixi commands. If specified, pixi will look for manifest files (pixi.toml or pyproject.toml) + in this directory instead of the repository root. Useful for monorepos where pixi projects are in subdirectories. run-install: description: Whether to run `pixi install` after installing pixi. Defaults to `true`. environments: diff --git a/dist/index.js b/dist/index.js index eb42f4a..536e615 100644 --- a/dist/index.js +++ b/dist/index.js @@ -80156,11 +80156,13 @@ var sha256 = (s) => { }; var execute = (cmd) => { core.debug(`Executing: \`${cmd.toString()}\``); - return (0, import_exec.exec)(`"${cmd[0]}"`, cmd.slice(1)); + return (0, import_exec.exec)(`"${cmd[0]}"`, cmd.slice(1), { cwd: options.workingDirectory }); }; -var executeGetOutput = (cmd, options2) => { +var executeGetOutput = (cmd, execOptions) => { core.debug(`Executing: \`${cmd.toString()}\``); - return (0, import_exec.getExecOutput)(`"${cmd[0]}"`, cmd.slice(1), options2); + const defaultOptions = { cwd: options.workingDirectory }; + const mergedOptions = execOptions ? { ...defaultOptions, ...execOptions } : defaultOptions; + return (0, import_exec.getExecOutput)(`"${cmd[0]}"`, cmd.slice(1), mergedOptions); }; var pixiCmd = (command, withManifestPath = true) => { let commandArray = [options.pixiBinPath].concat(command.split(" ").filter((x) => x !== "")); @@ -80344,26 +80346,31 @@ var inferOptions = (inputs) => { inputs.pixiBinPath ); const logLevel = inputs.logLevel ?? (core2.isDebug() ? "vv" : "default"); - let manifestPath = pixiPath; + const workingDirectory = inputs.workingDirectory ? import_path.default.resolve(untildify(inputs.workingDirectory)) : process.cwd(); + core2.debug(`Working directory: ${workingDirectory}`); + const pixiPathInWorkingDir = import_path.default.join(workingDirectory, pixiPath); + const pyprojectPathInWorkingDir = import_path.default.join(workingDirectory, pyprojectPath); + let manifestPath = pixiPathInWorkingDir; if (inputs.manifestPath) { - manifestPath = import_path.default.resolve(untildify(inputs.manifestPath)); + manifestPath = import_path.default.isAbsolute(inputs.manifestPath) ? import_path.default.resolve(untildify(inputs.manifestPath)) : import_path.default.resolve(workingDirectory, untildify(inputs.manifestPath)); } else { - if ((0, import_fs.existsSync)(pixiPath)) { - manifestPath = pixiPath; - } else if ((0, import_fs.existsSync)(pyprojectPath)) { + if ((0, import_fs.existsSync)(pixiPathInWorkingDir)) { + manifestPath = pixiPathInWorkingDir; + core2.debug(`Found pixi.toml at: ${manifestPath}`); + } else if ((0, import_fs.existsSync)(pyprojectPathInWorkingDir)) { try { - const fileContent = (0, import_fs.readFileSync)(pyprojectPath, "utf-8"); + const fileContent = (0, import_fs.readFileSync)(pyprojectPathInWorkingDir, "utf-8"); const parsedContent = parse3(fileContent); if (parsedContent.tool && typeof parsedContent.tool === "object" && "pixi" in parsedContent.tool) { - core2.debug(`The tool.pixi table found, using ${pyprojectPath} as manifest file.`); - manifestPath = pyprojectPath; + core2.debug(`The tool.pixi table found, using ${pyprojectPathInWorkingDir} as manifest file.`); + manifestPath = pyprojectPathInWorkingDir; } } catch (error3) { - core2.error(`Error while trying to read ${pyprojectPath} file.`); + core2.error(`Error while trying to read ${pyprojectPathInWorkingDir} file.`); core2.error(error3); } } else if (runInstall) { - core2.warning(`Could not find any manifest file. Defaulting to ${pixiPath}.`); + core2.warning(`Could not find any manifest file in ${workingDirectory}. Defaulting to ${pixiPathInWorkingDir}.`); } } const pixiLockFile = import_path.default.join(import_path.default.dirname(manifestPath), "pixi.lock"); @@ -80417,6 +80424,7 @@ var inferOptions = (inputs) => { downloadPixi: downloadPixi2, logLevel, manifestPath, + workingDirectory, pixiLockFile, runInstall, environments: inputs.environments, @@ -80447,6 +80455,7 @@ var getOptions = () => { "log-level must be one of `q`, `default`, `v`, `vv`, `vvv`." ), manifestPath: parseOrUndefined("manifest-path", string2()), + workingDirectory: parseOrUndefined("working-directory", string2()), runInstall: parseOrUndefinedJSON("run-install", boolean2()), environments: parseOrUndefinedList("environments", string2()), activateEnvironment: parseOrUndefined("activate-environment", string2()), @@ -80522,7 +80531,7 @@ var generateProjectCacheKey = async (cacheKeyPrefix) => { core3.debug(`lockfilePathSha: ${lockfilePathSha}`); const environments = sha256(options.environments?.join(" ") ?? ""); core3.debug(`environments: ${environments}`); - const cwdSha = sha256(process.cwd()); + const cwdSha = sha256(options.workingDirectory); core3.debug(`cwdSha: ${cwdSha}`); const sha = sha256(lockfileSha + environments + pixiSha2 + lockfilePathSha + cwdSha); core3.debug(`sha: ${sha}`); diff --git a/dist/post.js b/dist/post.js index 734f2bf..0ed20f6 100644 --- a/dist/post.js +++ b/dist/post.js @@ -30014,26 +30014,31 @@ var inferOptions = (inputs) => { inputs.pixiBinPath ); const logLevel = inputs.logLevel ?? (core2.isDebug() ? "vv" : "default"); - let manifestPath = pixiPath; + const workingDirectory = inputs.workingDirectory ? import_path.default.resolve(untildify(inputs.workingDirectory)) : process.cwd(); + core2.debug(`Working directory: ${workingDirectory}`); + const pixiPathInWorkingDir = import_path.default.join(workingDirectory, pixiPath); + const pyprojectPathInWorkingDir = import_path.default.join(workingDirectory, pyprojectPath); + let manifestPath = pixiPathInWorkingDir; if (inputs.manifestPath) { - manifestPath = import_path.default.resolve(untildify(inputs.manifestPath)); + manifestPath = import_path.default.isAbsolute(inputs.manifestPath) ? import_path.default.resolve(untildify(inputs.manifestPath)) : import_path.default.resolve(workingDirectory, untildify(inputs.manifestPath)); } else { - if ((0, import_fs.existsSync)(pixiPath)) { - manifestPath = pixiPath; - } else if ((0, import_fs.existsSync)(pyprojectPath)) { + if ((0, import_fs.existsSync)(pixiPathInWorkingDir)) { + manifestPath = pixiPathInWorkingDir; + core2.debug(`Found pixi.toml at: ${manifestPath}`); + } else if ((0, import_fs.existsSync)(pyprojectPathInWorkingDir)) { try { - const fileContent = (0, import_fs.readFileSync)(pyprojectPath, "utf-8"); + const fileContent = (0, import_fs.readFileSync)(pyprojectPathInWorkingDir, "utf-8"); const parsedContent = parse3(fileContent); if (parsedContent.tool && typeof parsedContent.tool === "object" && "pixi" in parsedContent.tool) { - core2.debug(`The tool.pixi table found, using ${pyprojectPath} as manifest file.`); - manifestPath = pyprojectPath; + core2.debug(`The tool.pixi table found, using ${pyprojectPathInWorkingDir} as manifest file.`); + manifestPath = pyprojectPathInWorkingDir; } } catch (error2) { - core2.error(`Error while trying to read ${pyprojectPath} file.`); + core2.error(`Error while trying to read ${pyprojectPathInWorkingDir} file.`); core2.error(error2); } } else if (runInstall) { - core2.warning(`Could not find any manifest file. Defaulting to ${pixiPath}.`); + core2.warning(`Could not find any manifest file in ${workingDirectory}. Defaulting to ${pixiPathInWorkingDir}.`); } } const pixiLockFile = import_path.default.join(import_path.default.dirname(manifestPath), "pixi.lock"); @@ -30087,6 +30092,7 @@ var inferOptions = (inputs) => { downloadPixi, logLevel, manifestPath, + workingDirectory, pixiLockFile, runInstall, environments: inputs.environments, @@ -30117,6 +30123,7 @@ var getOptions = () => { "log-level must be one of `q`, `default`, `v`, `vv`, `vvv`." ), manifestPath: parseOrUndefined("manifest-path", string2()), + workingDirectory: parseOrUndefined("working-directory", string2()), runInstall: parseOrUndefinedJSON("run-install", boolean2()), environments: parseOrUndefinedList("environments", string2()), activateEnvironment: parseOrUndefined("activate-environment", string2()), diff --git a/src/cache.ts b/src/cache.ts index ab75b6e..6b42c62 100644 --- a/src/cache.ts +++ b/src/cache.ts @@ -32,8 +32,8 @@ export const generateProjectCacheKey = async (cacheKeyPrefix: string) => { core.debug(`lockfilePathSha: ${lockfilePathSha}`) const environments = sha256(options.environments?.join(' ') ?? '') core.debug(`environments: ${environments}`) - // since the lockfile path is not necessarily absolute, we need to include the cwd in the cache key - const cwdSha = sha256(process.cwd()) + // since the lockfile path is not necessarily absolute, we need to include the working directory in the cache key + const cwdSha = sha256(options.workingDirectory) core.debug(`cwdSha: ${cwdSha}`) const sha = sha256(lockfileSha + environments + pixiSha + lockfilePathSha + cwdSha) core.debug(`sha: ${sha}`) diff --git a/src/options.ts b/src/options.ts index 879b762..2728652 100644 --- a/src/options.ts +++ b/src/options.ts @@ -15,6 +15,7 @@ type Inputs = Readonly<{ pixiUrlHeaders?: NodeJS.Dict logLevel?: LogLevel manifestPath?: string + workingDirectory?: string runInstall?: boolean environments?: string[] activateEnvironment?: string @@ -80,6 +81,7 @@ export type Options = Readonly<{ downloadPixi: boolean logLevel: LogLevel manifestPath: string + workingDirectory: string pixiLockFile: string runInstall: boolean environments?: string[] @@ -281,29 +283,41 @@ const inferOptions = (inputs: Inputs): Options => { inputs.pixiBinPath ) const logLevel = inputs.logLevel ?? (core.isDebug() ? 'vv' : 'default') - // infer manifest path from inputs or default to pixi.toml or pyproject.toml depending on what is present in the repo. - let manifestPath = pixiPath // default + + // Determine the working directory - resolve to absolute path if provided + const workingDirectory = inputs.workingDirectory ? path.resolve(untildify(inputs.workingDirectory)) : process.cwd() + core.debug(`Working directory: ${workingDirectory}`) + + // infer manifest path from inputs or default to pixi.toml or pyproject.toml depending on what is present in the working directory. + const pixiPathInWorkingDir = path.join(workingDirectory, pixiPath) + const pyprojectPathInWorkingDir = path.join(workingDirectory, pyprojectPath) + + let manifestPath = pixiPathInWorkingDir // default if (inputs.manifestPath) { - manifestPath = path.resolve(untildify(inputs.manifestPath)) + // If manifest path is provided, resolve it relative to working directory if it's not absolute + manifestPath = path.isAbsolute(inputs.manifestPath) + ? path.resolve(untildify(inputs.manifestPath)) + : path.resolve(workingDirectory, untildify(inputs.manifestPath)) } else { - if (existsSync(pixiPath)) { - manifestPath = pixiPath - } else if (existsSync(pyprojectPath)) { + if (existsSync(pixiPathInWorkingDir)) { + manifestPath = pixiPathInWorkingDir + core.debug(`Found pixi.toml at: ${manifestPath}`) + } else if (existsSync(pyprojectPathInWorkingDir)) { try { - const fileContent = readFileSync(pyprojectPath, 'utf-8') + const fileContent = readFileSync(pyprojectPathInWorkingDir, 'utf-8') const parsedContent: Record = parse(fileContent) // Test if the tool.pixi table is present in the pyproject.toml file, if so, use it as the manifest file. if (parsedContent.tool && typeof parsedContent.tool === 'object' && 'pixi' in parsedContent.tool) { - core.debug(`The tool.pixi table found, using ${pyprojectPath} as manifest file.`) - manifestPath = pyprojectPath + core.debug(`The tool.pixi table found, using ${pyprojectPathInWorkingDir} as manifest file.`) + manifestPath = pyprojectPathInWorkingDir } } catch (error) { - core.error(`Error while trying to read ${pyprojectPath} file.`) + core.error(`Error while trying to read ${pyprojectPathInWorkingDir} file.`) core.error(error as Error) } } else if (runInstall) { - core.warning(`Could not find any manifest file. Defaulting to ${pixiPath}.`) + core.warning(`Could not find any manifest file in ${workingDirectory}. Defaulting to ${pixiPathInWorkingDir}.`) } } @@ -372,6 +386,7 @@ const inferOptions = (inputs: Inputs): Options => { downloadPixi, logLevel, manifestPath, + workingDirectory, pixiLockFile, runInstall, environments: inputs.environments, @@ -410,6 +425,7 @@ const getOptions = () => { 'log-level must be one of `q`, `default`, `v`, `vv`, `vvv`.' ), manifestPath: parseOrUndefined('manifest-path', z.string()), + workingDirectory: parseOrUndefined('working-directory', z.string()), runInstall: parseOrUndefinedJSON('run-install', z.boolean()), environments: parseOrUndefinedList('environments', z.string()), activateEnvironment: parseOrUndefined('activate-environment', z.string()), diff --git a/src/util.ts b/src/util.ts index d2b1028..d51e2ad 100644 --- a/src/util.ts +++ b/src/util.ts @@ -82,14 +82,16 @@ export const execute = (cmd: string[]) => { core.debug(`Executing: \`${cmd.toString()}\``) // needs escaping if cmd[0] contains spaces // https://github.com/prefix-dev/setup-pixi/issues/184#issuecomment-2765724843 - return exec(`"${cmd[0]}"`, cmd.slice(1)) + return exec(`"${cmd[0]}"`, cmd.slice(1), { cwd: options.workingDirectory }) } -export const executeGetOutput = (cmd: string[], options?: ExecOptions) => { +export const executeGetOutput = (cmd: string[], execOptions?: ExecOptions) => { core.debug(`Executing: \`${cmd.toString()}\``) // needs escaping if cmd[0] contains spaces // https://github.com/prefix-dev/setup-pixi/issues/184#issuecomment-2765724843 - return getExecOutput(`"${cmd[0]}"`, cmd.slice(1), options) + const defaultOptions = { cwd: options.workingDirectory } + const mergedOptions = execOptions ? { ...defaultOptions, ...execOptions } : defaultOptions + return getExecOutput(`"${cmd[0]}"`, cmd.slice(1), mergedOptions) } export const pixiCmd = (command: string, withManifestPath = true) => {