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:
parent
0f7f1d08e3
commit
a3e458a5fa
136
LOCAL_SETUP_INSTRUCTIONS.md
Normal file
136
LOCAL_SETUP_INSTRUCTIONS.md
Normal 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.
|
||||
```
|
||||
@ -1,4 +1,4 @@
|
||||
import * as utils from './utils';
|
||||
import * as utils from './local_utils';
|
||||
|
||||
/**
|
||||
* Add script to set custom ini values for unix
|
||||
|
||||
@ -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';
|
||||
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import * as utils from './utils';
|
||||
import * as utils from './local_utils';
|
||||
|
||||
/**
|
||||
* Install and enable extensions for darwin
|
||||
|
||||
89
src/fetch.ts
89
src/fetch.ts
@ -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
151
src/local_installer.ts
Normal 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
272
src/local_utils.ts
Normal 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.');
|
||||
@ -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>;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user