use std::path::{Path, PathBuf};
use anyhow::{Context, Result, bail};
use serde::Deserialize;
use crate::fetch;
use crate::versions::Version;
const STATIC_PHP_BASE: &str = "https://dl.static-php.dev";
const STATIC_PHP_INDEX: &str = "https://dl.static-php.dev/static-php-cli/common/?format=json";
const WINDOWS_INDEX: &str = "https://downloads.php.net/~windows/releases/releases.json";
const COMPOSER_URL: &str = "https://getcomposer.org/download/latest-stable/composer.phar";
pub struct AvailableBuild {
pub version: Version,
url: String,
sha256: Option<String>,
}
#[derive(Debug, Deserialize)]
struct StaticPhpEntry {
is_dir: bool,
full_path: String,
name: String,
}
fn fetch_static_php() -> Result<Vec<AvailableBuild>> {
let os = match std::env::consts::OS {
"macos" => "macos",
"linux" => "linux",
other => bail!("unsupported platform for php: {other}"),
};
let arch = match std::env::consts::ARCH {
"x86_64" => "x86_64",
"aarch64" => "aarch64",
other => bail!("unsupported architecture for php: {other}"),
};
let suffix = format!("-cli-{os}-{arch}.tar.gz");
let entries: Vec<StaticPhpEntry> = fetch::client()?
.get(STATIC_PHP_INDEX)
.send()
.context("failed to query dl.static-php.dev")?
.error_for_status()
.context("static-php index query failed")?
.json()
.context("failed to parse static-php index")?;
let mut builds: Vec<AvailableBuild> = entries
.into_iter()
.filter(|e| !e.is_dir)
.filter_map(|e| {
let version: Version = e
.name
.strip_prefix("php-")?
.strip_suffix(&suffix)?
.parse()
.ok()?;
Some(AvailableBuild {
version,
url: format!("{STATIC_PHP_BASE}{}", e.full_path),
sha256: None,
})
})
.collect();
builds.sort_by_key(|b| b.version);
Ok(builds)
}
#[derive(Debug, Deserialize)]
struct WindowsZip {
path: String,
sha256: String,
}
fn fetch_windows_php() -> Result<Vec<AvailableBuild>> {
if std::env::consts::ARCH != "x86_64" {
bail!("windows.php.net publishes x64 builds only; no official arm64 PHP for Windows yet");
}
let index: std::collections::HashMap<String, serde_json::Value> = fetch::client()?
.get(WINDOWS_INDEX)
.send()
.context("failed to query windows.php.net releases")?
.error_for_status()
.context("windows php release query failed")?
.json()
.context("failed to parse windows php releases")?;
let mut builds: Vec<AvailableBuild> = index
.into_values()
.filter_map(|branch| {
let zip: WindowsZip =
serde_json::from_value(branch.get("nts-vs17-x64")?.get("zip")?.clone()).ok()?;
let version: Version = branch.get("version")?.as_str()?.parse().ok()?;
Some(AvailableBuild {
version,
url: format!("https://downloads.php.net/~windows/releases/{}", zip.path),
sha256: Some(zip.sha256.to_ascii_lowercase()),
})
})
.collect();
builds.sort_by_key(|b| b.version);
Ok(builds)
}
pub fn fetch_available() -> Result<Vec<AvailableBuild>> {
if std::env::consts::OS == "windows" {
fetch_windows_php()
} else {
fetch_static_php()
}
}
pub fn install_build(build: &AvailableBuild, dest: &Path) -> Result<()> {
let http = fetch::client()?;
let archive_name = build.url.rsplit('/').next().unwrap_or(&build.url);
eprintln!("downloading {}", build.url);
let archive = fetch::download(&http, &build.url)?;
match &build.sha256 {
Some(expected) => fetch::verify_sha256(&archive, expected, archive_name)?,
None => eprintln!("warning: no published checksum for this build; skipping verification"),
}
fetch::extract_archive_root(&archive, archive_name, dest)?;
eprintln!("downloading {COMPOSER_URL}");
let phar = fetch::download(&http, COMPOSER_URL)?;
let sums = http
.get(format!("{COMPOSER_URL}.sha256sum"))
.send()
.and_then(|r| r.error_for_status())
.context("failed to fetch composer checksum")?
.text()?;
let expected = sums
.split_whitespace()
.next()
.context("empty composer checksum")?;
fetch::verify_sha256(&phar, expected, "composer.phar")?;
std::fs::write(dest.join("composer.phar"), &phar)
.with_context(|| format!("failed to write {}", dest.join("composer.phar").display()))?;
if cfg!(windows) {
std::fs::write(
dest.join("composer.bat"),
"@echo off\r\n\"%~dp0php.exe\" \"%~dp0composer.phar\" %*\r\n",
)?;
} else {
let wrapper = dest.join("composer");
std::fs::write(
&wrapper,
"#!/bin/sh\nexec \"$(dirname \"$0\")/php\" \"$(dirname \"$0\")/composer.phar\" \"$@\"\n",
)?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(&wrapper, std::fs::Permissions::from_mode(0o755))?;
}
}
Ok(())
}
pub fn bin_dir(toolchain: &Path) -> PathBuf {
toolchain.to_path_buf()
}
pub fn php_exe() -> &'static str {
if cfg!(windows) { "php.exe" } else { "php" }
}