feat: Enable local PHP setup outside GitHub Actions

This commit introduces a mechanism to use the setup-php scripts for configuring PHP environments on local Linux/Ubuntu machines.

Key changes:
- Added `src/local_installer.ts`: A Node.js script that takes command-line arguments to define the desired PHP setup (version, extensions, tools, INI settings) and generates a `run.sh` script.
- Added `src/local_utils.ts`: A utility module with functions adapted for local execution, removing dependencies on `@actions/core`. It supports fetching inputs from yargs/environment variables.
- Modified `src/fetch.ts`: Replaced `@actions/http-client` with `node-fetch` for compatibility.
- Updated `src/config.ts`, `src/coverage.ts`, `src/extensions.ts`, and `src/tools.ts` to import utilities from `local_utils.ts` instead of `utils.ts`.
- Added `LOCAL_SETUP_INSTRUCTIONS.md`: Comprehensive documentation on prerequisites, how to run the local installer, command-line options, and troubleshooting.

This allows users to leverage the robust PHP environment configuration capabilities of this project directly on their development machines.
This commit is contained in:
google-labs-jules[bot] 2025-06-24 06:07:03 +00:00
parent 0f7f1d08e3
commit a3e458a5fa
8 changed files with 598 additions and 58 deletions

136
LOCAL_SETUP_INSTRUCTIONS.md Normal file
View File

@ -0,0 +1,136 @@
# Local PHP Environment Setup using setup-php Scripts
This document provides instructions on how to use the `setup-php` action's scripts to configure a PHP environment on your local Linux/Ubuntu machine, outside of GitHub Actions.
This process involves using a Node.js script (`src/local_installer.ts`) to generate a shell script (`run.sh`) tailored to your desired PHP setup. You then execute `run.sh` to perform the actual installation and configuration.
## Prerequisites
1. **Linux/Ubuntu Environment:** These instructions are primarily for Ubuntu. Debian or other Ubuntu derivatives might work but are not explicitly tested.
2. **Node.js and npm:** Required to run the `local_installer.ts` script.
* Install Node.js (which includes npm) from [NodeSource](https://github.com/nodesource/distributions#installation-instructions) or your distribution's package manager. Version 16+ is recommended.
3. **Curl:** Used by the scripts to download PHP versions and tools.
```bash
sudo apt update
sudo apt install curl
```
4. **Build Tools (Recommended):** For installing PECL extensions or some PHP versions from source.
```bash
sudo apt install -y build-essential autoconf automake bison re2c libtool make pkg-config
```
5. **Git:** To clone the repository.
```bash
sudo apt install -y git
```
6. **Sudo Access:** The generated `run.sh` script will require `sudo` privileges to install packages and configure PHP.
## Setup Steps
1. **Clone the `setup-php` Repository:**
```bash
git clone https://github.com/shivammathur/setup-php.git
cd setup-php
```
2. **Install Node.js Dependencies:**
This will install `yargs` (for command-line argument parsing by `local_installer.ts`) and `node-fetch` (used by the adapted `src/fetch.ts`).
```bash
npm install yargs node-fetch@2
# If your project uses ES Modules and you prefer node-fetch v3:
# npm install yargs node-fetch
```
*Note: `node-fetch@2` is CJS compatible which is generally easier with `ts-node`. If you have an ESM setup, v3+ might be used.*
3. **Compile TypeScript (Optional but Recommended):**
While you can run `.ts` files directly with `ts-node`, compiling them to JavaScript first is a cleaner approach for repeated use.
```bash
npm install -g typescript ts-node # Install TypeScript and ts-node globally if not already
# Or as dev dependencies: npm install --save-dev typescript ts-node @types/node @types/yargs
tsc --project tsconfig.json # Ensure your tsconfig.json is set up for CJS output to a 'dist' folder typically
```
If you compile, the command in the next step will change (e.g. `node dist/local_installer.js`). For simplicity, the following steps will use `ts-node`. If you compile, adjust the path accordingly.
4. **Run `local_installer.ts` to Generate `run.sh`:**
Execute `src/local_installer.ts` using `ts-node`, providing your desired PHP configuration as command-line arguments.
**Command Syntax:**
```bash
npx ts-node src/local_installer.ts [options]
```
**Available Options:**
* `--php-version <version>`: PHP version (e.g., `8.2`, `7.4`, `latest`, `nightly`). Default: `latest`.
* `--php-version-file <path>`: Path to a `.php-version` or similar file. Default: `.php-version`.
* `--extensions <csv_list>`: Comma-separated list of PHP extensions (e.g., `mbstring,gd,intl,zip`). Default: `""`.
* `--ini-file <type>`: Base `php.ini` to use (`production`, `development`, `none`). Default: `production`.
* `--ini-values <csv_list>`: Comma-separated list of `php.ini` values (e.g., `memory_limit=512M,post_max_size=128M`). Default: `""`.
* `--coverage <driver>`: Code coverage driver (`xdebug`, `pcov`, `none`). Default: `""`.
* `--tools <csv_list>`: Comma-separated list of tools to install (e.g., `composer,phpunit:latest,php-cs-fixer`). Default: `composer`.
* `--fail-fast <boolean>`: Exit immediately if a tool or extension fails. Default: `false`.
* `--phpts <type>`: PHP thread safety (`nts` or `zts`/`ts`). Default: `nts`.
* `--update <boolean>`: Force update PHP to the latest patch version. Default: `false`.
* `--debug <boolean>`: Install debug build of PHP. Default: `false`.
* `--tools-dir <path>`: Directory for installing global tools. Default: `/usr/local/bin`.
* `--runner-tool-cache <path>`: Directory for caching downloaded tools (simulates GitHub Actions `RUNNER_TOOL_CACHE`). Default: `/opt/hostedtoolcache`. (Ensure this directory is writable by the user running `sudo bash run.sh` or create it with appropriate permissions: `sudo mkdir -p /opt/hostedtoolcache && sudo chmod -R 777 /opt/hostedtoolcache`)
**Example:**
To set up PHP 8.2 with extensions `mbstring`, `gd`, `zip`, `intl` and tools `composer` and `phpunit` (latest version):
```bash
npx ts-node src/local_installer.ts \
--php-version 8.2 \
--extensions "mbstring,gd,zip,intl" \
--tools "composer,phpunit:latest" \
--ini-values "memory_limit=256M,date.timezone=UTC"
```
This will generate a `run.sh` file in your current directory (`setup-php`).
5. **Review and Execute `run.sh`:**
Inspect the generated `run.sh` script to understand the commands that will be executed.
```bash
less run.sh
```
Execute the script with `sudo` using `bash`:
```bash
sudo bash run.sh
```
The script will then proceed to install and configure PHP and the specified extensions and tools. This may take some time.
6. **Verify Installation:**
After the script finishes, verify your PHP setup:
```bash
php -v
php -m # List compiled modules (extensions)
composer --version # If installed
phpunit --version # If installed
```
## Environment Variables for Composer (Optional)
If you use private Composer repositories or hit GitHub API rate limits, you might need to set authentication environment variables **before running `sudo bash run.sh`**:
* **`GITHUB_TOKEN`**: Your GitHub Personal Access Token for Composer to access private GitHub repositories or increase rate limits.
```bash
export GITHUB_TOKEN="your_github_pat"
# Then run: sudo -E bash run.sh (the -E preserves user environment variables)
```
* **`PACKAGIST_TOKEN`**: For private Packagist.
```bash
export PACKAGIST_TOKEN="your_packagist_token"
# Then run: sudo -E bash run.sh
```
* **`COMPOSER_AUTH_JSON`**: For other types of Composer authentication, as a JSON string.
```bash
export COMPOSER_AUTH_JSON='{"http-basic": {"example.org": {"username": "user", "password": "password"}}}'
# Then run: sudo -E bash run.sh
```
It's often better to pass these to `sudo` using the `-E` flag if you set them in your user shell: `sudo -E bash run.sh`.
## Troubleshooting
* **Script Failures:** If `run.sh` fails, examine the output for error messages. These often indicate missing system dependencies (e.g., `-dev` library packages for a PECL extension) or network issues.
* **Permissions:** Ensure the `RUNNER_TOOL_CACHE` directory (default `/opt/hostedtoolcache`) is writable. The `local_installer.ts` script will print a message if it's using this. You might need to create and set permissions for it: `sudo mkdir -p /opt/hostedtoolcache && sudo chown -R $(whoami):$(whoami) /opt/hostedtoolcache` (or `sudo chmod -R 777 /opt/hostedtoolcache`).
* **`fetch.ts` / `node-fetch`:** The adapted `src/fetch.ts` uses `node-fetch`. Ensure it's installed correctly.
* **Path Issues:** After installation, if tools like `composer` are not found, ensure that the `tools-dir` (default `/usr/local/bin`) is in your system's `PATH`. The scripts attempt to add relevant paths to shell profiles, but you might need to source your profile (`source ~/.bashrc`) or open a new terminal.
This local setup mechanism provides a powerful way to replicate parts of the `setup-php` GitHub Action's functionality directly on your Ubuntu machine.
```

View File

@ -1,4 +1,4 @@
import * as utils from './utils';
import * as utils from './local_utils';
/**
* Add script to set custom ini values for unix

View File

@ -1,4 +1,4 @@
import * as utils from './utils';
import * as utils from './local_utils';
import * as extensions from './extensions';
import * as config from './config';

View File

@ -1,4 +1,4 @@
import * as utils from './utils';
import * as utils from './local_utils';
/**
* Install and enable extensions for darwin

View File

@ -1,56 +1,37 @@
import {IncomingMessage, OutgoingHttpHeaders} from 'http';
import * as https from 'https';
import * as url from 'url';
// This is a modified version of fetch.ts for local execution, using node-fetch.
// User should install node-fetch: npm install node-fetch@2
// (or node-fetch@3 if using ESM modules, but this project seems to be CJS for now)
import OriginalNodeFetch from 'node-fetch';
/**
* Function to fetch a URL
*
* @param input_url
* @param auth_token
* @param redirect_count
*/
export async function fetch(
input_url: string,
auth_token?: string,
redirect_count = 5
): Promise<Record<string, string>> {
const fetch_promise: Promise<Record<string, string>> = new Promise(
resolve => {
const url_object: url.UrlObject = new url.URL(input_url);
const headers: OutgoingHttpHeaders = {
'User-Agent': `Mozilla/5.0 (${process.platform} ${process.arch}) setup-php`
};
if (auth_token) {
headers.authorization = 'Bearer ' + auth_token;
}
const options: https.RequestOptions = {
hostname: url_object.hostname,
path: url_object.pathname,
headers: headers,
agent: new https.Agent({keepAlive: false})
};
const req = https.get(options, (res: IncomingMessage) => {
if (res.statusCode === 200) {
let body = '';
res.setEncoding('utf8');
res.on('data', chunk => (body += chunk));
res.on('end', () => resolve({data: `${body}`}));
} else if (
[301, 302, 303, 307, 308].includes(res.statusCode as number)
) {
if (redirect_count > 0 && res.headers.location) {
fetch(res.headers.location, auth_token, redirect_count--).then(
resolve
);
} else {
resolve({error: `${res.statusCode}: Redirect error`});
}
} else {
resolve({error: `${res.statusCode}: ${res.statusMessage}`});
}
});
req.end();
}
);
return await fetch_promise;
// node-fetch v3 is ESM-only. If the project is CJS, we need v2 or a dynamic import.
// For simplicity, assuming node-fetch v2 or that the project can handle ESM.
// If using ts-node with commonjs, node-fetch v2 is easier.
const fetchModule = OriginalNodeFetch as any; // Type assertion to handle CJS/ESM differences if any
const actualFetch = fetchModule.default || fetchModule; // Handle default export for ESM
interface FetchResult {
data: string | null;
statusCode: number;
error?: string;
}
export async function fetch(url: string): Promise<FetchResult> {
try {
const response = await actualFetch(url, {
// node-fetch options if needed, e.g., timeout, retries (though http-client handled retries)
// For simplicity, no complex retry logic here, but could be added.
});
const data = await response.text();
if (response.ok) {
return { data, statusCode: response.status };
}
return { data, statusCode: response.status, error: `Request failed with status ${response.status}` };
} catch (error: any) {
// console.error(`Fetch error for URL ${url}:`, error);
return {
data: null,
statusCode: 0, // Indicate a client-side error or network issue
error: error.message
};
}
}

151
src/local_installer.ts Normal file
View File

@ -0,0 +1,151 @@
import path from 'path';
import fs from 'fs';
import * as yargs from 'yargs';
import * as config from './config';
import * as coverage from './coverage';
import * as extensions from './extensions';
import * as tools from './tools';
import * as local_utils from './local_utils';
// Helper function to get input from yargs argv.
// local_utils.getInput will now handle the logic of checking yargs_argv or env.
function getYargsInput(argv: yargs.ArgumentsCamelCase<any>, name: string, defaultValue?: string): string {
// This function is primarily for direct use within local_installer.ts
// For sub-modules, they should use local_utils.getInput()
const value = argv[name.replace(/-/g, '')];
if (value === undefined || value === null) {
return defaultValue !== undefined ? defaultValue : '';
}
return String(value);
}
/**
* Build the script
*/
async function getScript(os: string, argv: yargs.ArgumentsCamelCase<any>): Promise<string> {
const url = 'https://setup-php.com/sponsor';
const filename = os + (await local_utils.scriptExtension(os));
const script_path = path.join(path.resolve(__dirname, '../src/scripts'), filename);
const run_path = path.resolve(process.cwd(), 'run.sh');
// Set environment variables from CLI options for shell scripts and local_utils.getInput as fallback
Object.keys(argv).forEach(key => {
if (key !== '_' && key !== '$0') {
// Convert camelCase from yargs to UPPER_SNAKE_CASE for env vars, as scripts might expect that
// e.g. phpVersion -> PHP_VERSION. This also helps local_utils.getInput via readEnv.
const envKey = key.replace(/([A-Z])/g, '_$1').toUpperCase();
process.env[envKey] = String(argv[key]);
}
});
// Explicitly set some critical env vars that scripts use with specific names
process.env['FAIL_FAST'] = getYargsInput(argv, 'fail-fast', 'false');
process.env['PHPTS'] = getYargsInput(argv, 'phpts', 'nts');
process.env['UPDATE'] = getYargsInput(argv, 'update', 'false');
process.env['DEBUG'] = getYargsInput(argv, 'debug', 'false');
process.env['RUNNER'] = getYargsInput(argv, 'runner', 'self-hosted');
process.env['SETUP_PHP_TOOLS_DIR'] = getYargsInput(argv, 'tools-dir', '/usr/local/bin');
process.env['RUNNER_TOOL_CACHE'] = getYargsInput(argv, 'runner-tool-cache', '/opt/hostedtoolcache');
process.env['GITHUB_WORKSPACE'] = process.cwd(); // Default GITHUB_WORKSPACE
delete process.env['GITHUB_ACTIONS']; // Ensure GITHUB_ACTIONS is not inadvertently set
// These values are passed to the main functions of sub-modules
// If those modules still use utils.getInput for *other* parameters,
// local_utils.getInput will try to find them in process.env or the initialized yargs_argv
const extension_csv: string = getYargsInput(argv, 'extensions', '');
const ini_values_csv: string = getYargsInput(argv, 'ini-values', '');
const coverage_driver: string = getYargsInput(argv, 'coverage', '');
const tools_csv: string = getYargsInput(argv, 'tools', 'composer');
const phpVersionInput: string = getYargsInput(argv, 'php-version', 'latest');
const phpVersionFile: string = getYargsInput(argv, 'php-version-file', '.php-version');
const version: string = await local_utils.parseVersion(
await local_utils.readPHPVersion(phpVersionInput, phpVersionFile)
);
const ini_file: string = await local_utils.parseIniFile(
getYargsInput(argv, 'ini-file', 'production')
);
// Use logging functions from local_utils
let script = await local_utils.joins('.', script_path, version, ini_file);
if (extension_csv) {
// Assuming extensions.addExtension and others will use local_utils.getInput() internally
// or are refactored to take all their params.
// For now, rely on local_utils.getInput() being able to access process.env or yargs_argv
script += await extensions.addExtension(extension_csv, version, os);
}
script += await tools.addTools(tools_csv, version, os);
if (coverage_driver) {
script += await coverage.addCoverage(coverage_driver, version, os);
}
if (ini_values_csv) {
script += await config.addINIValues(ini_values_csv, os);
}
script += '\n' + (await local_utils.stepLog(`Sponsor setup-php`, os));
script += '\n' + (await local_utils.addLog('$tick', 'setup-php', url, os));
fs.writeFileSync(run_path, script, {mode: 0o755});
return run_path;
}
/**
* Main function to generate the script
*/
async function main(): Promise<void> {
const argv = await yargs.default(process.argv.slice(2))
.usage('Usage: $0 [options]')
.option('php-version', { type: 'string', default: 'latest', description: 'PHP version (e.g., 8.2, latest, nightly)' })
.option('php-version-file', { type: 'string', default: '.php-version', description: 'Path to .php-version file' })
.option('extensions', { type: 'string', default: '', description: 'CSV list of PHP extensions (e.g., mbstring,gd)' })
.option('ini-file', { type: 'string', default: 'production', choices: ['production', 'development', 'none'], description: 'Base php.ini file to use' })
.option('ini-values', { type: 'string', default: '', description: 'CSV list of php.ini values (e.g., memory_limit=256M)' })
.option('coverage', { type: 'string', default: '', choices: ['', 'xdebug', 'pcov', 'none'], description: 'Code coverage driver' })
.option('tools', { type: 'string', default: 'composer', description: 'CSV list of tools to install (e.g., composer,phpunit)' })
.option('fail-fast', { type: 'boolean', default: false, description: 'Exit immediately if a tool or extension fails to install' })
.option('phpts', { type: 'string', default: 'nts', choices: ['nts', 'zts', 'ts'], description: 'PHP thread safety (nts or zts/ts)' })
.option('update', { type: 'boolean', default: false, description: 'Force update PHP to the latest patch version' })
.option('debug', { type: 'boolean', default: false, description: 'Install debug build of PHP' })
.option('runner', { type: 'string', default: 'self-hosted', hidden: true, description: 'Runner type (self-hosted or github)' })
.option('tools-dir', { type: 'string', default: '/usr/local/bin', description: 'Directory for installing tools' })
.option('runner-tool-cache', { type: 'string', default: '/opt/hostedtoolcache', description: 'Directory for caching tools (like RUNNER_TOOL_CACHE)'})
.help('h')
.alias('h', 'help')
.argv;
local_utils.initYargs(argv); // Initialize yargs arguments for local_utils
const os: string = process.platform;
if (os !== 'linux') {
console.error('This script is intended for Linux/Ubuntu environments.');
process.exit(1);
}
try {
const generatedScriptPath = await getScript(os, argv);
console.log(`Setup script generated at: ${generatedScriptPath}`);
console.log(`You can now run it using: sudo bash ${generatedScriptPath}`);
console.log(`Important: Ensure that the following environment variables are set if your setup requires them:`);
console.log(` COMPOSER_AUTH_JSON, GITHUB_TOKEN, PACKAGIST_TOKEN (for private composer repositories)`);
console.log(` Any other environment variables specific to your PHP application or build process.`);
} catch (error: any) {
console.error('Error generating script:', error.message);
if (error.stack) {
console.error(error.stack);
}
process.exit(1);
}
}
// Call the main function
main().catch(error => {
console.error('Unhandled error in main:', error.message);
if (error.stack) {
console.error(error.stack);
}
process.exit(1);
});

272
src/local_utils.ts Normal file
View File

@ -0,0 +1,272 @@
import fs from 'fs';
import * as path from 'path';
import * as fetch from './fetch'; // Now uses the node-fetch based version
let yargs_argv: Record<string, any> | null = null;
/**
* Initialize yargs arguments for utils to use.
* Call this from local_installer.ts after parsing argv.
*/
export function initYargs(argv: Record<string, any>): void {
yargs_argv = argv;
}
/**
* Function to read environment variable and return a string value.
*/
export async function readEnv(property: string): Promise<string> {
const property_lc: string = property.toLowerCase();
const property_uc: string = property.toUpperCase();
// Check specific env var first, then the yargs-provided one if it maps to an env-like name
const envValue =
process.env[property] ||
process.env[property_lc] ||
process.env[property_uc] ||
process.env[property_lc.replace(/-/g, '_')] || // for vars like fail-fast
process.env[property_uc.replace(/-/g, '_')];
if (envValue !== undefined) {
return envValue;
}
// Fallback to checking yargs if it's initialized (for env vars that might be passed as CLI flags)
if (yargs_argv) {
const yargsValue = yargs_argv[property.toLowerCase().replace(/_/g, '')]; // e.g. PHPTS -> phpts
if (yargsValue !== undefined) {
return String(yargsValue);
}
}
return '';
}
/**
* Mock for core.getInput or read from process.env / yargs
* This is crucial for sub-modules that call utils.getInput()
*/
export async function getInput(name: string, mandatory: boolean = false): Promise<string> {
// Try yargs first if available (for direct CLI parameters)
if (yargs_argv) {
const yargsValue = yargs_argv[name.replace(/-/g, '')]; // yargs uses camelCase for kebab-case
if (yargsValue !== undefined) {
return String(yargsValue);
}
}
// Then try environment variables (local_installer.ts sets these)
const env_input = await readEnv(name);
if (env_input) {
return env_input;
}
if (mandatory) {
throw new Error(`Input required and not supplied: ${name}`);
}
return '';
}
/**
* Function to get manifest URL
*/
export async function getManifestURLS(): Promise<string[]> {
return [
'https://setup-php.com/php-versions.json',
'https://raw.githubusercontent.com/shivammathur/setup-php/develop/src/configs/php-versions.json'
];
}
/**
* Function to parse PHP version.
* @param versionInput The version string from CLI or default
*/
export async function parseVersion(versionInput: string): Promise<string> {
const version = versionInput.trim();
if (/^(latest|lowest|highest|nightly|\d+\.x)$/.test(version)) {
for (const manifestURL of await getManifestURLS()) {
try {
const fetchResult = await fetch.fetch(manifestURL);
if (fetchResult.data && fetchResult.statusCode === 200) {
const manifest = JSON.parse(fetchResult.data);
if (manifest[version]) {
return manifest[version];
}
} else {
console.warn(`Warning: Failed to fetch manifest from ${manifestURL}, status: ${fetchResult.statusCode}`);
}
} catch (error: any) {
console.warn(`Warning: Could not fetch PHP version manifest from ${manifestURL}: ${error.message}`);
}
}
console.warn(`Warning: Could not resolve version alias '${version}' from any manifest.`);
// Provide some fallback for critical aliases if all manifests fail
if (version === 'latest' || version === 'highest') return '8.2'; // Example fallback
if (version === 'lowest') return '5.6'; // Example fallback
throw new Error(`Could not resolve version alias '${version}' from PHP version manifest after trying all sources.`);
} else {
if (/^\d+$/.test(version)) return version + '.0';
if (/^\d+\.\d+$/.test(version)) return version;
if (/^\d+\.\d+\.\d+.*$/.test(version)) {
const parts = version.split('.');
return parts[0] + '.' + parts[1];
}
return version; // Return as is if not a known alias or simple pattern
}
}
/**
* Function to parse ini file.
*/
export async function parseIniFile(ini_file: string): Promise<string> {
if (/^(production|development|none)$/.test(ini_file)) return ini_file;
if (/php\.ini-(production|development)$/.test(ini_file)) return ini_file.split('-')[1];
return 'production';
}
/**
* Read php version from input or file
*/
export async function readPHPVersion(versionArg: string, versionFileArg: string): Promise<string> {
if (versionArg && versionArg.toLowerCase() !== 'latest') {
return versionArg;
}
const versionFileToUse = versionFileArg || '.php-version';
if (fs.existsSync(versionFileToUse)) {
const contents: string = fs.readFileSync(versionFileToUse, 'utf8');
const match: RegExpMatchArray | null = contents.match(/(?:php\s*)?(\d+\.\d+(?:\.\d+)?(?:[\w.-]*))/m);
if (match && match[1]) return match[1].trim();
const trimmedContents = contents.trim();
if (trimmedContents) return trimmedContents;
} else if (versionFileArg && versionFileArg !== '.php-version') {
console.warn(`Warning: Specified --php-version-file '${versionFileArg}' not found.`);
}
const composerProjectDir = await readEnv('COMPOSER_PROJECT_DIR') || process.cwd();
const composerLock = path.join(composerProjectDir, 'composer.lock');
if (fs.existsSync(composerLock)) {
try {
const lockFileContents = JSON.parse(fs.readFileSync(composerLock, 'utf8'));
if (lockFileContents?.['platform-overrides']?.['php']) {
return lockFileContents['platform-overrides']['php'];
}
} catch (e: any) { console.warn(`Warning: Could not parse ${composerLock}: ${e.message}`); }
}
const composerJson = path.join(composerProjectDir, 'composer.json');
if (fs.existsSync(composerJson)) {
try {
const composerFileContents = JSON.parse(fs.readFileSync(composerJson, 'utf8'));
if (composerFileContents?.['config']?.['platform']?.['php']) {
return composerFileContents['config']['platform']['php'];
}
} catch (e: any) { console.warn(`Warning: Could not parse ${composerJson}: ${e.message}`); }
}
return versionArg || 'latest'; // Return original arg if it was 'latest', or 'latest' if it was empty
}
/**
* Function to join strings with space
*/
export async function joins(...str: string[]): Promise<string> {
return [...str].join(' ');
}
/**
* Function to get script extensions
*/
export async function scriptExtension(os: string): Promise<string> {
if (os === 'win32') return '.ps1';
if (os === 'linux' || os === 'darwin') return '.sh';
throw new Error('Platform ' + os + ' is not supported');
}
// Logging utilities - these generate script snippets that call shell functions
export async function stepLog(message: string, os: string): Promise<string> {
// The shell scripts (unix.sh, win32.ps1) define step_log / Step-Log
if (os === 'win32') return `Step-Log "${message}"`;
return `step_log "${message}"`; // For linux/darwin
}
export async function addLog(mark: string, subject: string, message: string, os: string): Promise<string> {
// The shell scripts (unix.sh, win32.ps1) define add_log / Add-Log
// The $tick / $cross are shell variables defined in unix.sh
if (os === 'win32') return `Add-Log "${mark}" "${subject}" "${message}"`;
return `add_log "${mark}" "${subject}" "${message}"`; // For linux/darwin
}
// Other utilities used by modules like extensions.ts, tools.ts
export async function suppressOutput(os: string): Promise<string> {
if (os === 'win32') return ' >$null 2>&1';
return ' >/dev/null 2>&1';
}
export async function getCommand(os: string, suffix: string): Promise<string> {
if (os === 'linux' || os === 'darwin') return 'add_' + suffix + ' ';
if (os === 'win32') {
return ('Add-' + suffix.split('_').map((part: string) => part.charAt(0).toUpperCase() + part.slice(1)).join('') + ' ');
}
throw new Error('Platform ' + os + ' is not supported for getCommand');
}
export async function getExtensionPrefix(extension: string): Promise<string> {
return /xdebug([2-3])?$|opcache|ioncube|eaccelerator/.test(extension) ? 'zend_extension' : 'extension';
}
export async function extensionArray(extension_csv: string): Promise<Array<string>> {
if (!extension_csv?.trim()) return [];
return [
extension_csv.match(/(^|,\s?)none(\s?,|$)/) ? 'none' : '',
...extension_csv.split(',').map(ext => {
if (/.+-.+\/.+@.+/.test(ext)) return ext; // For source extensions
return ext.trim().toLowerCase().replace(/^(:)?(php[-_]|none|zend )|(-[^-]*)-/, '$1$3');
})
].filter(Boolean);
}
export async function CSVArray(values_csv: string): Promise<Array<string>> {
if (!values_csv?.trim()) return [];
return values_csv
.split(/,(?=(?:(?:[^"']*["']){2})*[^"']*$)/)
.map(value => value.trim().replace(/^["']|["']$|(?<==)["']/g, '').replace(/=(((?!E_).)*[?{}|&~![()^]+((?!E_).)+)/, "='$1'").replace(/=(.*?)(=.*)/, "='$1$2'").replace(/:\s*["'](.*?)/g, ':$1'))
.filter(Boolean);
}
export async function getUnsupportedLog(extension: string, version: string, os: string): Promise<string> {
return ('\n' + (await addLog('$cross', extension, `${extension} is not supported on PHP ${version}`, os)) + '\n');
}
export async function customPackage(pkg: string, type: string, version: string, os: string): Promise<string> {
const pkg_name: string = pkg.replace(/\d+|(pdo|pecl)[_-]/, '');
const ext: string = await scriptExtension(os);
// __dirname in local_utils.ts will be .../src/ or .../dist/
// The scripts are in ../src/scripts relative to install.ts/local_installer.ts
// or ../scripts relative to local_utils.ts if it's in the same dir as local_installer.ts
const scriptPath = path.resolve(__dirname, '../src/scripts', type, `${pkg_name}${ext}`);
if (!fs.existsSync(scriptPath)) {
// Fallback if src/scripts is not found relative to utils (e.g. if utils is in dist)
const altScriptPath = path.resolve(__dirname, '../../src/scripts', type, `${pkg_name}${ext}`);
if (fs.existsSync(altScriptPath)) {
return `\n. ${altScriptPath}\n${await getCommand(os, pkg_name)}${version}`;
}
console.warn(`Script not found for custom package: ${scriptPath} or ${altScriptPath}`);
return `\necho "Error: Script for ${pkg_name} not found."\n`;
}
return `\n. ${scriptPath}\n${await getCommand(os, pkg_name)}${version}`;
}
export async function parseExtensionSource(extension: string, prefix: string): Promise<string> {
const regex = /(\w+)-(\w+:\/\/.{1,253}(?:[.:][^:/\s]{2,63})+\/)?([\w.-]+)\/([\w.-]+)@(.+)/;
const matches = regex.exec(extension) as RegExpExecArray;
if (!matches) throw new Error(`Invalid extension source format: ${extension}`);
matches[2] = matches[2] ? matches[2].slice(0, -1) : 'https://github.com';
return await joins('\nadd_extension_from_source', ...matches.slice(1), prefix);
}
export async function setVariable(variable: string, command: string, os: string): Promise<string> {
if (os === 'win32') return `\n$${variable} = ${command}\n`;
return `\n${variable}="$(${command})"\n`; // For linux/darwin
}
// It's important that local_installer.ts calls initYargs(argv)
// so that getInput can access the parsed command line arguments.
console.log('local_utils.ts loaded. Ensure initYargs is called from local_installer.ts.');

View File

@ -2,7 +2,7 @@ import path from 'path';
import fs from 'fs';
import * as fetch from './fetch';
import * as packagist from './packagist';
import * as utils from './utils';
import * as utils from './local_utils';
type RS = Record<string, string>;
type RSRS = Record<string, RS>;