-
Notifications
You must be signed in to change notification settings - Fork 0
[codex] Add project-level PHP extension opt-ins #289
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
f752890
4ef50d3
7c2fd57
09bee62
8dd68eb
838ebf4
115e90c
465ae81
23e94b6
b5902b4
024fb3f
799b067
1f6da72
05b8036
6a7904f
b8c733c
3894539
11b0610
a1abefe
b8f9965
9ecbecc
b08a092
f2a6234
1cc5698
cbb5fe0
7d10838
44cc1f1
b4c16c6
a28178a
ef6f9bb
f4bb620
1edef98
b2db34c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,34 @@ | ||
| # Task 2 Report: PHP Extension Metadata And Overlay Helpers | ||
|
|
||
| ## Status | ||
|
|
||
| DONE_WITH_CONCERNS | ||
|
|
||
| ## Summary | ||
|
|
||
| - Added `resources::php_extensions` with artifact metadata parsing, request resolution, runtime overlay writing, and runtime environment helpers. | ||
| - Exported `PhpExtensionModule`, `PhpExtensionLoadKind`, `PhpExtensionResolution`, `PHP_EXTENSION_METADATA_PATH`, and the new helper functions from `resources`. | ||
| - Added optional `php_extensions` artifact metadata parsing to `ManifestArtifact` with a public `php_extensions()` accessor. | ||
| - Added integration coverage for metadata resolution and runtime overlay generation. | ||
| - Updated manifest snapshots to include parsed PHP extension metadata and default empty metadata for older manifests. | ||
|
|
||
| ## TDD Notes | ||
|
|
||
| - Wrote `crates/resources/tests/php_extensions.rs` and extended `manifest_foundation.rs` before implementation. | ||
| - Red check failed as expected with missing exports and missing `ManifestArtifact::php_extensions()`. | ||
| - Implemented the minimal resources-side helpers and manifest parsing needed to satisfy the tests. | ||
|
|
||
| ## Verification | ||
|
|
||
| - PASS: `cargo nextest run -p resources -E 'test(resolves_available_and_ignored_php_extensions_from_artifact_metadata) or test(writes_runtime_overlay_for_loaded_php_extensions) or test(manifest_parses_registry_backed_resources_tracks_and_artifacts)'` | ||
| - PASS: `cargo nextest run -p resources --no-fail-fast` | ||
| - PASS: `cargo clippy -p resources --all-targets --all-features --locked -- -D warnings` | ||
| - EXPECTED FAIL: `cargo build --workspace --all-targets` fails only in known staged daemon callers using `Option<PhpConfig>::as_deref()` at `crates/daemon/src/project_env.rs:119` and `crates/daemon/src/gateway.rs:482`. | ||
|
|
||
| ## Scope Notes | ||
|
|
||
| - `crates/resources/src/runtime.rs` was listed in the task file, but no runtime adapter change was needed. Requiring PHP extension metadata during artifact validation would violate the approved design requirement that older PHP artifacts without metadata remain valid and are treated as supporting no optional extensions. | ||
|
|
||
| ## Concerns | ||
|
|
||
| - Workspace build remains blocked by the known Task 1 downstream `PhpConfig` API migration work outside this task's scope. |
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,11 +4,11 @@ use std::io; | |
| use std::io::Write; | ||
| use std::process::ExitCode; | ||
|
|
||
| use camino::Utf8PathBuf; | ||
| use camino::{Utf8Path, Utf8PathBuf}; | ||
| use resources::{ | ||
| ArtifactManifestCache, ManagedResourceCommands, ManagedResourceUninstallOptions, | ||
| ResourceAdapter, ResourceHttpClient, ResourceName, TargetPlatform, TrackName, TrackSelector, | ||
| UreqResourceHttpClient, | ||
| ArtifactManifestCache, ConcreteTrackName, ManagedResourceCommands, | ||
| ManagedResourceUninstallOptions, ResourceAdapter, ResourceHttpClient, ResourceName, | ||
| TargetPlatform, TrackName, TrackSelector, UreqResourceHttpClient, | ||
| }; | ||
| use serde::Serialize; | ||
| use state::{Database, ManagedResourceDesiredState, ProjectRecord, PvPaths, StateError}; | ||
|
|
@@ -259,40 +259,209 @@ pub(crate) fn shim_with_args_and_env( | |
| ) -> Result<ExitCode, ExecuteError> { | ||
| let paths = pv_paths(environment)?; | ||
| let database = Database::open(&paths)?; | ||
| let track = resolve_php_track_for_shim(&paths, &database, environment)?; | ||
| let installed = installed_php(&database, &track)?; | ||
| resources::ensure_php_track_defaults(&paths, &track)?; | ||
| env.extend(resources::php_track_exec_environment(&paths, &track)?); | ||
| let runtime = resolve_php_runtime_for_shim(&paths, &database, environment)?; | ||
| let installed = installed_php(&database, &runtime.track)?; | ||
| resources::ensure_php_track_defaults(&paths, &runtime.track)?; | ||
| let loaded_modules = resources::resolve_persisted_php_extension_modules( | ||
| installed.release_path(), | ||
| &runtime.loaded_extensions, | ||
| )?; | ||
| env.extend(resources::php_runtime_exec_environment( | ||
| &paths, | ||
| &runtime.track, | ||
| &runtime.runtime_key, | ||
| installed.release_path(), | ||
| &loaded_modules, | ||
| )?); | ||
| let executable = installed.executable()?; | ||
|
|
||
| environment | ||
| .exec_with_env(executable.as_std_path(), &args, &env) | ||
| .map_err(ExecuteError::from) | ||
| } | ||
|
|
||
| fn resolve_php_track_for_shim( | ||
| struct PhpShimRuntime { | ||
| track: String, | ||
| runtime_key: String, | ||
| loaded_extensions: Vec<String>, | ||
| } | ||
|
|
||
| fn resolve_php_runtime_for_shim( | ||
| paths: &PvPaths, | ||
| database: &Database, | ||
| environment: &impl Environment, | ||
| ) -> Result<String, ExecuteError> { | ||
| ) -> Result<PhpShimRuntime, ExecuteError> { | ||
| let current_dir = current_dir(environment)?; | ||
| if let Some(project) = database.nearest_project_for_path(¤t_dir)? | ||
| && let Some(track) = project.desired_php_track | ||
| { | ||
| return Ok(track); | ||
| } | ||
| if let Some(project) = database.nearest_project_for_path(¤t_dir)? { | ||
| let config_file = match config::ProjectConfigFile::read_from_root(&project.path) { | ||
| Ok(config_file) => config_file, | ||
| Err(error) => { | ||
| if let Some(runtime) = persisted_project_php_runtime_for_shim(&project)? { | ||
| return Ok(runtime); | ||
| } | ||
|
|
||
| if let Some(track) = database.global_php_default_track()? { | ||
| return Ok(track); | ||
| return Err(error.into()); | ||
| } | ||
| }; | ||
| if let Some(track) = project.php_runtime.track.clone() { | ||
| let php = config_file.config.php.as_ref(); | ||
| let requested_extensions = php | ||
| .map(|php| php.requested_extensions().to_vec()) | ||
| .unwrap_or_default(); | ||
| let config_track = if let Some(php) = php { | ||
| match php.version_selector() { | ||
| Some(selector) if selector != "latest" && selector != track => Some( | ||
| resolve_project_config_php_track_for_shim(paths, database, php)?, | ||
| ), | ||
| None if database.global_php_default_track()?.is_some() | ||
| || paths.downloads().join("manifest.json").exists() => | ||
| { | ||
| let resolved_track = | ||
| resolve_project_config_php_track_for_shim(paths, database, php)?; | ||
| if resolved_track == track { | ||
| None | ||
| } else { | ||
| Some(resolved_track) | ||
| } | ||
| } | ||
| _ => None, | ||
| } | ||
| } else { | ||
| None | ||
| }; | ||
| let current_track = config_track.as_deref().unwrap_or(&track).to_string(); | ||
| if requested_extensions.is_empty() { | ||
|
Comment on lines
+329
to
+333
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
For a linked Project with no Useful? React with 👍 / 👎. |
||
| return Ok(PhpShimRuntime { | ||
| runtime_key: current_track.clone(), | ||
| track: current_track, | ||
| loaded_extensions: Vec::new(), | ||
| }); | ||
| } | ||
| if config_track.is_none() | ||
| && project.php_runtime.requested_extensions == requested_extensions | ||
|
munezaclovis marked this conversation as resolved.
|
||
| { | ||
| let runtime_key = | ||
| state::php_runtime_key(&track, &project.php_runtime.loaded_extensions)?; | ||
|
|
||
| return Ok(PhpShimRuntime { | ||
| track, | ||
| runtime_key, | ||
| loaded_extensions: project.php_runtime.loaded_extensions, | ||
| }); | ||
| } | ||
| if let Some(php) = php { | ||
| return resolve_project_config_php_runtime_for_shim(database, ¤t_track, php); | ||
| } | ||
| } else if let Some(php) = config_file.config.php.as_ref() { | ||
| let track = resolve_project_config_php_track_for_shim(paths, database, php)?; | ||
|
|
||
| return resolve_project_config_php_runtime_for_shim(database, &track, php); | ||
| } | ||
| } | ||
|
|
||
| let manifest = ArtifactManifestCache::new(paths.downloads()).load_cached()?; | ||
| let php = ResourceName::new("php")?; | ||
| let track = match database.global_php_default_track()? { | ||
| Some(track) => track, | ||
| None => { | ||
| let manifest = ArtifactManifestCache::new(paths.downloads()).load_cached()?; | ||
| let php = ResourceName::new("php")?; | ||
|
|
||
| Ok(manifest | ||
| .resolve_track(&php, TrackSelector::Latest)? | ||
| .as_str() | ||
| .to_string()) | ||
| manifest | ||
| .resolve_track(&php, TrackSelector::Latest)? | ||
| .as_str() | ||
| .to_string() | ||
| } | ||
| }; | ||
|
|
||
| Ok(PhpShimRuntime { | ||
| runtime_key: track.clone(), | ||
| track, | ||
| loaded_extensions: Vec::new(), | ||
| }) | ||
| } | ||
|
|
||
| fn persisted_project_php_runtime_for_shim( | ||
| project: &ProjectRecord, | ||
| ) -> Result<Option<PhpShimRuntime>, ExecuteError> { | ||
| let Some(track) = project.php_runtime.track.clone() else { | ||
| return Ok(None); | ||
| }; | ||
| let loaded_extensions = project.php_runtime.loaded_extensions.clone(); | ||
| let runtime_key = state::php_runtime_key(&track, &loaded_extensions)?; | ||
|
|
||
| Ok(Some(PhpShimRuntime { | ||
| track, | ||
| runtime_key, | ||
| loaded_extensions, | ||
| })) | ||
| } | ||
|
|
||
| fn resolve_project_config_php_track_for_shim( | ||
| paths: &PvPaths, | ||
| database: &Database, | ||
| php: &config::PhpConfig, | ||
| ) -> Result<String, ExecuteError> { | ||
| let selector = php | ||
| .version_selector() | ||
| .map(TrackSelector::parse) | ||
| .transpose()?; | ||
| let track = match selector { | ||
| Some(TrackSelector::Latest) => { | ||
| let manifest = ArtifactManifestCache::new(paths.downloads()).load_cached()?; | ||
| let php = ResourceName::new("php")?; | ||
|
|
||
| manifest | ||
| .resolve_track(&php, TrackSelector::Latest)? | ||
| .as_str() | ||
| .to_string() | ||
| } | ||
| Some(TrackSelector::Track(track)) => track.as_str().to_string(), | ||
| None => match database.global_php_default_track()? { | ||
| Some(track) => track, | ||
| None => { | ||
| let manifest = ArtifactManifestCache::new(paths.downloads()).load_cached()?; | ||
| let php = ResourceName::new("php")?; | ||
|
|
||
| manifest | ||
| .resolve_track(&php, TrackSelector::Latest)? | ||
| .as_str() | ||
| .to_string() | ||
| } | ||
| }, | ||
| }; | ||
| let track = ConcreteTrackName::new(track)?; | ||
|
|
||
| Ok(track.as_str().to_string()) | ||
| } | ||
|
|
||
| fn resolve_project_config_php_runtime_for_shim( | ||
| database: &Database, | ||
| track: &str, | ||
| php: &config::PhpConfig, | ||
| ) -> Result<PhpShimRuntime, ExecuteError> { | ||
| let requested_extensions = php.requested_extensions().to_vec(); | ||
| if requested_extensions.is_empty() { | ||
| return Ok(PhpShimRuntime { | ||
| runtime_key: track.to_string(), | ||
| track: track.to_string(), | ||
| loaded_extensions: Vec::new(), | ||
| }); | ||
| } | ||
|
|
||
| let installed = installed_php(database, track)?; | ||
| let resolution = | ||
| resources::resolve_php_extension_request(installed.release_path(), &requested_extensions)?; | ||
| let loaded_extensions = resolution | ||
| .loaded | ||
| .iter() | ||
| .map(|module| module.name.clone()) | ||
| .collect::<Vec<_>>(); | ||
| let runtime_key = state::php_runtime_key(track, &loaded_extensions)?; | ||
|
|
||
| Ok(PhpShimRuntime { | ||
| track: track.to_string(), | ||
| runtime_key, | ||
| loaded_extensions, | ||
| }) | ||
| } | ||
|
|
||
| fn effective_global_php_default_track( | ||
|
|
@@ -323,6 +492,10 @@ impl InstalledPhp { | |
|
|
||
| Ok(adapter.executable_path(&self.release)) | ||
| } | ||
|
|
||
| fn release_path(&self) -> &Utf8Path { | ||
| &self.release | ||
| } | ||
| } | ||
|
|
||
| fn installed_php(database: &Database, track: &str) -> Result<InstalledPhp, ExecuteError> { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
php.version: lateststill takes the_ => Nonebranch here, socurrent_trackfalls back to the persisted runtime track instead of the manifest-resolved default. The new regression test sets the manifest default to8.5but asserts execution of the persisted8.4release, which locks in the stale-track behavior this path was meant to prevent.Technical details