Repository: railwayapp/nixpacks
Pull request #410 (2022-08-17 20:13:49)

Remove legacy build plans
- All providers using new build plan format
- Remove all legacy build plan stuff




Patch:
diff --git a/src/nixpacks/plan/generator.rs b/src/nixpacks/plan/generator.rs
index 0e40f30..bb41e48 100644
--- a/src/nixpacks/plan/generator.rs
+++ b/src/nixpacks/plan/generator.rs
@@ -1,14 +1,6 @@
-use super::{
-    config::GeneratePlanConfig,
-    legacy_phase::{LegacyBuildPhase, LegacyInstallPhase, LegacySetupPhase, LegacyStartPhase},
-    BuildPlan, LegacyBuildPlan, PlanGenerator,
-};
+use super::{config::GeneratePlanConfig, BuildPlan, PlanGenerator};
 use crate::{
-    nixpacks::{
-        app::{App, StaticAssets},
-        environment::{Environment, EnvironmentVariables},
-        nix::pkg::Pkg,
-    },
+    nixpacks::{app::App, environment::Environment, nix::pkg::Pkg},
     providers::Provider,
 };
 use anyhow::{bail, Context, Ok, Result};
@@ -83,257 +75,36 @@ impl NixpacksBuildPlanGenerator<'_> {
             &self.config,
         );
 
+        let mut plan = BuildPlan::default();
+
         if let Some(provider) = self.matched_provider {
             if let Some(provider_build_plan) = provider.get_build_plan(app, environment)? {
-                let mut build_plan = BuildPlan::apply_config(&provider_build_plan, &config);
-
-                if !environment.get_variable_names().is_empty() {
-                    build_plan.add_variables(Environment::clone_variables(environment));
-                }
-
-                return Ok(build_plan);
+                plan = provider_build_plan;
             }
         }
 
-        self.get_build_plan_from_legacy_phases(app, environment)
-    }
-
-    fn get_build_plan_from_legacy_phases(
-        &self,
-        app: &App,
-        environment: &Environment,
-    ) -> Result<BuildPlan> {
-        let setup_phase = self
-            .get_setup_phase(app, environment)
-            .context("Getting setup phase")?;
-        let install_phase = self
-            .get_install_phase(app, environment)
-            .context("Generating install phase")?;
-        let build_phase = self
-            .get_build_phase(app, environment)
-            .context("Generating build phase")?;
-        let start_phase = self
-            .get_start_phase(app, environment)
-            .context("Generating start phase")?;
-        let variables = self
-            .get_variables(app, environment)
-            .context("Getting plan variables")?;
-        let static_assets = self
-            .get_static_assets(app, environment)
-            .context("Getting provider assets")?;
-
-        let legacy_plan = LegacyBuildPlan {
-            setup: Some(setup_phase),
-            install: Some(install_phase),
-            build: Some(build_phase),
-            start: Some(start_phase),
-            variables: Some(variables),
-            static_assets: Some(static_assets),
-            version: None,
-        };
-
-        let plan: BuildPlan = legacy_plan.into();
-        Ok(plan)
-    }
-
-    fn get_setup_phase(&self, app: &App, environment: &Environment) -> Result<LegacySetupPhase> {
-        let mut setup_phase: LegacySetupPhase = match self.matched_provider {
-            Some(provider) => provider.setup(app, environment)?.unwrap_or_default(),
-            None => LegacySetupPhase::default(),
-        };
-
-        let env_var_pkgs = environment
-            .get_config_variable("PKGS")
-            .map(|pkg_string| pkg_string.split(' ').map(Pkg::new).collect::<Vec<_>>())
-            .unwrap_or_default();
-
-        // Add custom user packages
-        let mut pkgs = [self.config.custom_pkgs.clone(), env_var_pkgs].concat();
-        setup_phase.add_pkgs(&mut pkgs);
-
-        let env_var_libs = environment
-            .get_config_variable("LIBS")
-            .map(|lib_string| {
-                lib_string
-                    .split(' ')
-                    .map(ToString::to_string)
-                    .collect::<Vec<String>>()
-            })
-            .unwrap_or_default();
-
-        // Add custom user libraries
-        let libs = [self.config.custom_libs.clone(), env_var_libs].concat();
-        setup_phase.add_libraries(libs);
-
-        let env_var_apt_pkgs = environment
-            .get_config_variable("APT_PKGS")
-            .map(|apt_pkgs_string| {
-                apt_pkgs_string
-                    .split(' ')
-                    .map(ToString::to_string)
-                    .collect::<Vec<String>>()
-            })
-            .unwrap_or_default();
-
-        // Add custom apt packages
-        let apt_pkgs = [self.config.custom_apt_pkgs.clone(), env_var_apt_pkgs].concat();
-        setup_phase.add_apt_pkgs(apt_pkgs);
-
-        if self.config.pin_pkgs {
-            setup_phase.set_archive(NIXPKGS_ARCHIVE.to_string());
-        }
-
-        Ok(setup_phase)
-    }
-
-    fn get_install_phase(
-        &self,
-        app: &App,
-        environment: &Environment,
-    ) -> Result<LegacyInstallPhase> {
-        let mut install_phase = match self.matched_provider {
-            Some(provider) => provider.install(app, environment)?.unwrap_or_default(),
-            None => LegacyInstallPhase::default(),
-        };
-
-        let mut env_install_cmd = None;
-        if let Some(install_cmd) = environment.get_config_variable("INSTALL_CMD") {
-            env_install_cmd = Some(vec![install_cmd]);
+        // The Procfile start command has precedence over the provider's start command
+        if let Some(procfile_start) = self.get_procfile_start_cmd(app)? {
+            plan.start_phase.as_mut().map(|start| {
+                start.cmd = Some(procfile_start);
+                start
+            });
         }
 
-        if let Some(install_cache_dirs) = environment.get_config_variable("INSTALL_CACHE_DIRS") {
-            let custom_install_cache_dirs = install_cache_dirs
-                .split(',')
-                .map(ToString::to_string)
-                .collect::<Vec<_>>();
-
-            install_phase.cache_directories = match install_phase.cache_directories {
-                Some(dirs) => Some([dirs, custom_install_cache_dirs].concat()),
-                None => Some(custom_install_cache_dirs),
+        // The Procfiles release command is append to the provider's build command
+        if let Some(procfile_release) = self.get_procfile_release_cmd(app)? {
+            if let Some(build) = plan.get_phase_mut("build") {
+                build.add_cmd(procfile_release);
             }
         }
 
-        // Start command priority
-        // - custom install command
-        // - environment variable
-        // - provider
-
-        install_phase.cmds = self
-            .config
-            .custom_install_cmd
-            .clone()
-            .or(env_install_cmd)
-            .or(install_phase.cmds);
+        plan = BuildPlan::apply_config(&plan, &config);
 
-        Ok(install_phase)
-    }
-
-    fn get_build_phase(&self, app: &App, environment: &Environment) -> Result<LegacyBuildPhase> {
-        let mut build_phase = match self.matched_provider {
-            Some(provider) => provider.build(app, environment)?.unwrap_or_default(),
-            None => LegacyBuildPhase::default(),
-        };
-
-        let mut env_build_cmd = None;
-        if let Some(build_cmd) = environment.get_config_variable("BUILD_CMD") {
-            env_build_cmd = Some(vec![build_cmd]);
+        if !environment.get_variable_names().is_empty() {
+            plan.add_variables(Environment::clone_variables(environment));
         }
 
-        if let Some(build_cache_dirs) = environment.get_config_variable("BUILD_CACHE_DIRS") {
-            let custom_build_cache_dirs = build_cache_dirs
-                .split(',')
-                .map(ToString::to_string)
-                .collect::<Vec<_>>();
-
-            build_phase.cache_directories = match build_phase.cache_directories {
-                Some(dirs) => Some([dirs, custom_build_cache_dirs].concat()),
-                None => Some(custom_build_cache_dirs),
-            }
-        }
-
-        // Build command priority
-        // - custom build command
-        // - environment variable
-        // - provider
-        build_phase.cmds = self
-            .config
-            .custom_build_cmd
-            .clone()
-            .or(env_build_cmd)
-            .or(build_phase.cmds);
-
-        // Release process type
-        if let Some(release_cmd) = self.get_procfile_release_cmd(app)? {
-            build_phase
-                .cmds
-                .clone()
-                .unwrap_or_default()
-                .push(release_cmd);
-        }
-        Ok(build_phase)
-    }
-
-    fn get_start_phase(&self, app: &App, environment: &Environment) -> Result<LegacyStartPhase> {
-        let procfile_cmd = self.get_procfile_start_cmd(app)?;
-
-        let mut start_phase = match self.matched_provider {
-            Some(provider) => provider.start(app, environment)?.unwrap_or_default(),
-            None => LegacyStartPhase::default(),
-        };
-
-        let env_start_cmd = environment.get_config_variable("START_CMD");
-
-        // Start command priority
-        // - custom start command
-        // - environment variable
-        // - procfile
-        // - provider
-        start_phase.cmd = self
-            .config
-            .custom_start_cmd
-            .clone()
-            .or_else(|| env_start_cmd.or_else(|| procfile_cmd.or(start_phase.cmd)));
-
-        // Allow the user to override the run image with an environment variable
-        if let Some(env_run_image) = environment.get_config_variable("RUN_IMAGE") {
-            // If the env var is "falsy", then unset the run image on the start phase
-            start_phase.run_image = match env_run_image.as_str() {
-                "0" | "false" => None,
-                img if img.is_empty() => None,
-                img => Some(img.to_owned()),
-            };
-        }
-
-        Ok(start_phase)
-    }
-
-    fn get_variables(&self, app: &App, environment: &Environment) -> Result<EnvironmentVariables> {
-        // Get a copy of the variables in the environment
-        let variables = Environment::clone_variables(environment);
-
-        let new_variables = match self.matched_provider {
-            Some(provider) => {
-                // Merge provider variables
-                let provider_variables = provider
-                    .environment_variables(app, environment)?
-                    .unwrap_or_default();
-                provider_variables.into_iter().chain(variables).collect()
-            }
-            None => variables,
-        };
-
-        Ok(new_variables)
-    }
-
-    fn get_static_assets(&self, app: &App, environment: &Environment) -> Result<StaticAssets> {
-        let static_assets = match self.matched_provider {
-            Some(provider) => provider
-                .static_assets(app, environment)?
-                .unwrap_or_default(),
-            None => StaticAssets::new(),
-        };
-
-        Ok(static_assets)
+        Ok(plan)
     }
 
     fn get_procfile_start_cmd(&self, app: &App) -> Result<Option<String>> {
diff --git a/src/nixpacks/plan/legacy_phase.rs b/src/nixpacks/plan/legacy_phase.rs
deleted file mode 100644
index 8ffb1fc..0000000
--- a/src/nixpacks/plan/legacy_phase.rs
+++ /dev/null
@@ -1,236 +0,0 @@
-use serde::{Deserialize, Serialize};
-
-use crate::nixpacks::{
-    images::{DEBIAN_SLIM_IMAGE, DEFAULT_BASE_IMAGE},
-    nix::pkg::Pkg,
-};
-
-#[serde_with::skip_serializing_none]
-#[derive(Serialize, Deserialize, Clone, Debug)]
-pub struct LegacySetupPhase {
-    pub pkgs: Vec<Pkg>,
-    pub archive: Option<String>,
-    pub libraries: Option<Vec<String>>,
-    pub apt_pkgs: Option<Vec<String>>,
-    pub cmds: Option<Vec<String>>,
-
-    #[serde(rename = "onlyIncludeFiles")]
-    pub only_include_files: Option<Vec<String>>,
-
-    #[serde(rename = "baseImage")]
-    pub base_image: String,
-}
-
-impl LegacySetupPhase {
-    pub fn new(pkgs: Vec<Pkg>) -> Self {
-        Self {
-            pkgs,
-            libraries: None,
-            apt_pkgs: None,
-            archive: None,
-            only_include_files: None,
-            base_image: DEFAULT_BASE_IMAGE.to_string(),
-            cmds: None,
-        }
-    }
-
-    pub fn add_file_dependency(&mut self, file: String) {
-        if let Some(ref mut files) = self.only_include_files {
-            files.push(file);
-        } else {
-            self.only_include_files = Some(vec![file]);
-        }
-    }
-
-    pub fn add_pkgs(&mut self, new_pkgs: &mut Vec<Pkg>) {
-        self.pkgs.append(new_pkgs);
-    }
-
-    pub fn set_archive(&mut self, archive: String) {
-        self.archive = Some(archive);
-    }
-
-    pub fn add_libraries(&mut self, mut lib: Vec<String>) {
-        if let Some(ref mut libraries) = self.libraries {
-            libraries.append(&mut lib);
-        } else {
-            self.libraries = Some(lib);
-        }
-    }
-
-    pub fn add_apt_pkgs(&mut self, mut apt_pkgs: Vec<String>) {
-        if let Some(ref mut apt_packages) = self.apt_pkgs {
-            apt_packages.append(&mut apt_pkgs);
-        } else {
-            self.apt_pkgs = Some(apt_pkgs);
-        }
-    }
-
-    pub fn add_cmd(&mut self, cmd: String) {
-        if let Some(ref mut cmds) = self.cmds {
-            cmds.push(cmd);
-        } else {
-            self.cmds = Some(vec![cmd]);
-        }
-    }
-}
-
-impl Default for LegacySetupPhase {
-    fn default() -> Self {
-        Self {
-            pkgs: Vec::new(),
-            libraries: None,
-            apt_pkgs: None,
-            archive: None,
-            only_include_files: None,
-            base_image: DEFAULT_BASE_IMAGE.to_string(),
-            cmds: None,
-        }
-    }
-}
-
-#[serde_with::skip_serializing_none]
-#[derive(Serialize, Deserialize, Default, Clone, Debug)]
-pub struct LegacyInstallPhase {
-    pub cmds: Option<Vec<String>>,
-
-    #[serde(rename = "onlyIncludeFiles")]
-    pub only_include_files: Option<Vec<String>>,
-
-    #[serde(rename = "cacheDirectories")]
-    pub cache_directories: Option<Vec<String>>,
-
-    pub paths: Option<Vec<String>>,
-}
-
-impl LegacyInstallPhase {
-    pub fn new(cmd: String) -> Self {
-        Self {
-            cmds: Some(vec![cmd]),
-            only_include_files: None,
-            cache_directories: None,
-            paths: None,
-        }
-    }
-
-    pub fn add_file_dependency(&mut self, file: String) {
-        if let Some(mut files) = self.only_include_files.clone() {
-            files.push(file);
-            self.only_include_files = Some(files);
-        } else {
-            self.only_include_files = Some(vec![file]);
-        }
-    }
-
-    pub fn add_cache_directory(&mut self, dir: String) {
-        if let Some(ref mut dirs) = self.cache_directories {
-            dirs.push(dir);
-        } else {
-            self.cache_directories = Some(vec![dir]);
-        }
-    }
-
-    pub fn add_path(&mut self, path: String) {
-        if let Some(ref mut paths) = self.paths {
-            paths.push(path);
-        } else {
-            self.paths = Some(vec![path]);
-        }
-    }
-
-    pub fn add_cmd(&mut self, cmd: String) {
-        if let Some(ref mut cmds) = self.cmds {
-            cmds.push(cmd);
-        } else {
-            self.cmds = Some(vec![cmd]);
-        }
-    }
-}
-
-#[serde_with::skip_serializing_none]
-#[derive(Serialize, Deserialize, Default, Clone, Debug)]
-pub struct LegacyBuildPhase {
-    pub cmds: Option<Vec<String>>,
-
-    #[serde(rename = "onlyIncludeFiles")]
-    pub only_include_files: Option<Vec<String>>,
-
-    #[serde(rename = "cacheDirectories")]
-    pub cache_directories: Option<Vec<String>>,
-}
-
-impl LegacyBuildPhase {
-    pub fn new(cmd: String) -> Self {
-        Self {
-            cmds: Some(vec![cmd]),
-            only_include_files: None,
-            cache_directories: None,
-        }
-    }
-
-    pub fn add_file_dependency(&mut self, file: String) {
-        if let Some(ref mut files) = self.only_include_files {
-            files.push(file);
-        } else {
-            self.only_include_files = Some(vec![file]);
-        }
-    }
-
-    pub fn add_cache_directory(&mut self, dir: String) {
-        if let Some(ref mut dirs) = self.cache_directories {
-            dirs.push(dir);
-        } else {
-            self.cache_directories = Some(vec![dir]);
-        }
-    }
-
-    pub fn add_cmd(&mut self, cmd: String) {
-        if let Some(ref mut cmds) = self.cmds {
-            cmds.push(cmd);
-        } else {
-            self.cmds = Some(vec![cmd]);
-        }
-    }
-}
-
-#[serde_with::skip_serializing_none]
-#[derive(Serialize, Deserialize, Default, Clone, Debug)]
-pub struct LegacyStartPhase {
-    pub cmd: Option<String>,
-
-    #[serde(rename = "runImage")]
-    pub run_image: Option<String>,
-
-    #[serde(rename = "onlyIncludeFiles")]
-    pub only_include_files: Option<Vec<String>>,
-}
-
-impl LegacyStartPhase {
-    pub fn new(cmd: String) -> Self {
-        Self {
-            cmd: Some(cmd),
-            run_image: None,
-            only_include_files: None,
-        }
-    }
-
-    pub fn run_in_image(&mut self, image_name: String) {
-        self.run_image = Some(image_name);
-    }
-
-    pub fn run_in_default_image(&mut self) {
-        self.run_image = Some(DEFAULT_BASE_IMAGE.to_string());
-    }
-
-    pub fn run_in_slim_image(&mut self) {
-        self.run_image = Some(DEBIAN_SLIM_IMAGE.to_string());
-    }
-
-    pub fn add_file_dependency(&mut self, file: String) {
-        if let Some(ref mut files) = self.only_include_files {
-            files.push(file);
-        } else {
-            self.only_include_files = Some(vec![file]);
-        }
-    }
-}
diff --git a/src/nixpacks/plan/mod.rs b/src/nixpacks/plan/mod.rs
index fb2457d..d539508 100644
--- a/src/nixpacks/plan/mod.rs
+++ b/src/nixpacks/plan/mod.rs
@@ -1,7 +1,6 @@
 use self::{
     config::GeneratePlanConfig,
     generator::NIXPKGS_ARCHIVE,
-    legacy_phase::{LegacyBuildPhase, LegacyInstallPhase, LegacySetupPhase, LegacyStartPhase},
     phase::{Phase, StartPhase},
     topological_sort::topological_sort,
 };
@@ -17,7 +16,6 @@ use serde::{Deserialize, Serialize};
 
 pub mod config;
 pub mod generator;
-pub mod legacy_phase;
 pub mod phase;
 pub mod pretty_print;
 mod topological_sort;
@@ -26,18 +24,6 @@ pub trait PlanGenerator {
     fn generate_plan(&mut self, app: &App, environment: &Environment) -> Result<BuildPlan>;
 }
 
-#[serde_with::skip_serializing_none]
-#[derive(Debug, Serialize, Deserialize)]
-pub struct LegacyBuildPlan {
-    pub version: Option<String>,
-    pub setup: Option<LegacySetupPhase>,
-    pub install: Option<LegacyInstallPhase>,
-    pub build: Option<LegacyBuildPhase>,
-    pub start: Option<LegacyStartPhase>,
-    pub variables: Option<EnvironmentVariables>,
-    pub static_assets: Option<StaticAssets>,
-}
-
 #[serde_with::skip_serializing_none]
 #[derive(Debug, Serialize, Deserialize, Clone)]
 pub struct BuildPlan {
@@ -90,6 +76,19 @@ impl BuildPlan {
         }
     }
 
+    pub fn add_static_assets(&mut self, static_assets: StaticAssets) {
+        match self.static_assets.as_mut() {
+            Some(assets) => {
+                for (key, value) in &static_assets {
+                    assets.insert(key.to_string(), value.to_string());
+                }
+            }
+            None => {
+                self.static_assets = Some(static_assets);
+            }
+        }
+    }
+
     pub fn get_phase(&self, name: &str) -> Option<&Phase> {
         self.phases.iter().find(|phase| phase.name == name)
     }
@@ -188,24 +187,6 @@ impl Default for BuildPlan {
     }
 }
 
-impl From<LegacyBuildPlan> for BuildPlan {
-    fn from(legacy_plan: LegacyBuildPlan) -> Self {
-        let phases: Vec<Phase> = vec![
-            legacy_plan.setup.unwrap_or_default().into(),
-            legacy_plan.install.unwrap_or_default().into(),
-            legacy_plan.build.unwrap_or_default().into(),
-        ];
-
-        let start: StartPhase = legacy_plan.start.unwrap_or_default().into();
-
-        let mut plan = BuildPlan::new(phases, Some(start));
-        plan.static_assets = legacy_plan.static_assets;
-        plan.variables = legacy_plan.variables;
-
-        plan
-    }
-}
-
 fn none_if_empty<T>(value: Vec<T>) -> Option<Vec<T>> {
     if value.is_empty() {
         None
diff --git a/src/nixpacks/plan/phase.rs b/src/nixpacks/plan/phase.rs
index d76806a..400b45f 100644
--- a/src/nixpacks/plan/phase.rs
+++ b/src/nixpacks/plan/phase.rs
@@ -5,10 +5,7 @@ use crate::nixpacks::{
     nix::pkg::Pkg,
 };
 
-use super::{
-    legacy_phase::{LegacyBuildPhase, LegacyInstallPhase, LegacySetupPhase, LegacyStartPhase},
-    topological_sort::TopItem,
-};
+use super::topological_sort::TopItem;
 
 #[serde_with::skip_serializing_none]
 #[derive(Serialize, Deserialize, Default, Clone, Debug)]
@@ -149,6 +146,10 @@ impl Phase {
     pub fn add_path(&mut self, path: String) {
         self.paths = Some(add_to_option_vec(self.paths.clone(), path));
     }
+
+    pub fn set_nix_archive(&mut self, archive: String) {
+        self.nixpacks_archive = Some(archive);
+    }
 }
 
 impl StartPhase {
@@ -195,61 +196,3 @@ fn add_multiple_to_option_vec<T: Clone>(values: Option<Vec<T>>, new_values: Vec<
         new_values
     }
 }
-
-// Legacy intos
-
-impl From<LegacySetupPhase> for Phase {
-    fn from(setup_phase: LegacySetupPhase) -> Self {
-        Phase {
-            name: "setup".to_string(),
-            nix_pkgs: Some(setup_phase.pkgs),
-            nix_libraries: setup_phase.libraries,
-            nixpacks_archive: setup_phase.archive,
-            apt_pkgs: setup_phase.apt_pkgs,
-            cmds: setup_phase.cmds,
-            only_include_files: setup_phase.only_include_files,
-            ..Default::default()
-        }
-    }
-}
-
-impl From<LegacyInstallPhase> for Phase {
-    fn from(install_phase: LegacyInstallPhase) -> Self {
-        let mut i = Phase {
-            name: "install".to_string(),
-            cmds: install_phase.cmds,
-            only_include_files: install_phase.only_include_files,
-            cache_directories: install_phase.cache_directories,
-            paths: install_phase.paths,
-            ..Default::default()
-        };
-
-        i.depends_on_phase("setup");
-        i
-    }
-}
-
-impl From<LegacyBuildPhase> for Phase {
-    fn from(build_phase: LegacyBuildPhase) -> Self {
-        let mut p = Phase {
-            name: "build".to_string(),
-            cmds: build_phase.cmds,
-            only_include_files: build_phase.only_include_files,
-            cache_directories: build_phase.cache_directories,
-            ..Default::default()
-        };
-
-        p.depends_on_phase("install");
-        p
-    }
-}
-
-impl From<LegacyStartPhase> for StartPhase {
-    fn from(start_phase: LegacyStartPhase) -> Self {
-        StartPhase {
-            run_image: start_phase.run_image,
-            cmd: start_phase.cmd,
-            ..Default::default()
-        }
-    }
-}
diff --git a/src/providers/csharp.rs b/src/providers/csharp.rs
index badedeb..46aa29e 100644
--- a/src/providers/csharp.rs
+++ b/src/providers/csharp.rs
@@ -3,8 +3,9 @@ use crate::nixpacks::{
     app::App,
     environment::{Environment, EnvironmentVariables},
     nix::pkg::Pkg,
-    plan::legacy_phase::{
-        LegacyBuildPhase, LegacyInstallPhase, LegacySetupPhase, LegacyStartPhase,
+    plan::{
+        phase::{Phase, StartPhase},
+        BuildPlan,
     },
 };
 use anyhow::{Context, Result};
@@ -22,40 +23,24 @@ impl Provider for CSharpProvider {
         Ok(!app.find_files("*.csproj")?.is_empty())
     }
 
-    fn setup(&self, _app: &App, _env: &Environment) -> Result<Option<LegacySetupPhase>> {
-        Ok(Some(LegacySetupPhase::new(vec![Pkg::new("dotnet-sdk")])))
-    }
-
-    fn install(&self, _app: &App, _env: &Environment) -> Result<Option<LegacyInstallPhase>> {
-        Ok(Some(LegacyInstallPhase::new("dotnet restore".to_string())))
-    }
-
-    fn build(&self, _app: &App, _env: &Environment) -> Result<Option<LegacyBuildPhase>> {
-        Ok(Some(LegacyBuildPhase::new(format!(
+    fn get_build_plan(&self, app: &App, _env: &Environment) -> Result<Option<BuildPlan>> {
+        let setup = Phase::setup(Some(vec![Pkg::new("dotnet-sdk")]));
+        let install = Phase::install(Some("dotnet restore".to_string()));
+        let build = Phase::build(Some(format!(
             "dotnet publish --no-restore -c Release -o {}",
             ARTIFACT_DIR
-        ))))
-    }
+        )));
 
-    fn start(&self, app: &App, _env: &Environment) -> Result<Option<LegacyStartPhase>> {
         let csproj = &app.find_files("*.csproj")?[0].with_extension("");
         let project_name = csproj
             .file_name()
             .context("Invalid file_name")?
             .to_str()
             .context("Invalid project_name")?;
-        Ok(Some(LegacyStartPhase::new(format!(
-            "./{}/{}",
-            ARTIFACT_DIR, project_name
-        ))))
-    }
+        let start = StartPhase::new(format!("./{}/{}", ARTIFACT_DIR, project_name));
 
-    fn environment_variables(
-        &self,
-        _app: &App,
-        _env: &Environment,
-    ) -> Result<Option<EnvironmentVariables>> {
-        let env_vars = EnvironmentVariables::from([
+        let mut plan = BuildPlan::new(vec![setup, install, build], Some(start));
+        plan.add_variables(EnvironmentVariables::from([
             (
                 "ASPNETCORE_ENVIRONMENT".to_string(),
                 "Production".to_string(),
@@ -68,7 +53,8 @@ impl Provider for CSharpProvider {
                 "DOTNET_ROOT".to_string(),
                 "/nix/var/nix/profiles/default/".to_string(),
             ),
-        ]);
-        Ok(Some(env_vars))
+        ]));
+
+        Ok(Some(plan))
     }
 }
diff --git a/src/providers/fsharp.rs b/src/providers/fsharp.rs
index 7a86aa5..6e084b1 100644
--- a/src/providers/fsharp.rs
+++ b/src/providers/fsharp.rs
@@ -3,8 +3,9 @@ use crate::nixpacks::{
     app::App,
     environment::{Environment, EnvironmentVariables},
     nix::pkg::Pkg,
-    plan::legacy_phase::{
-        LegacyBuildPhase, LegacyInstallPhase, LegacySetupPhase, LegacyStartPhase,
+    plan::{
+        phase::{Phase, StartPhase},
+        BuildPlan,
     },
 };
 use anyhow::{Context, Result};
@@ -22,40 +23,24 @@ impl Provider for FSharpProvider {
         Ok(!app.find_files("*.fsproj")?.is_empty())
     }
 
-    fn setup(&self, _app: &App, _env: &Environment) -> Result<Option<LegacySetupPhase>> {
-        Ok(Some(LegacySetupPhase::new(vec![Pkg::new("dotnet-sdk")])))
-    }
-
-    fn install(&self, _app: &App, _env: &Environment) -> Result<Option<LegacyInstallPhase>> {
-        Ok(Some(LegacyInstallPhase::new("dotnet restore".to_string())))
-    }
-
-    fn build(&self, _app: &App, _env: &Environment) -> Result<Option<LegacyBuildPhase>> {
-        Ok(Some(LegacyBuildPhase::new(format!(
+    fn get_build_plan(&self, app: &App, _env: &Environment) -> Result<Option<BuildPlan>> {
+        let setup = Phase::setup(Some(vec![Pkg::new("dotnet-sdk")]));
+        let install = Phase::install(Some("dotnet restore".to_string()));
+        let build = Phase::build(Some(format!(
             "dotnet publish --no-restore -c Release -o {}",
             ARTIFACT_DIR
-        ))))
-    }
+        )));
 
-    fn start(&self, app: &App, _env: &Environment) -> Result<Option<LegacyStartPhase>> {
         let fsproj = &app.find_files("*.fsproj")?[0].with_extension("");
         let project_name = fsproj
             .file_name()
             .context("Invalid file_name")?
             .to_str()
             .context("Invalid project_name")?;
-        Ok(Some(LegacyStartPhase::new(format!(
-            "./{}/{}",
-            ARTIFACT_DIR, project_name
-        ))))
-    }
+        let start = StartPhase::new(format!("./{}/{}", ARTIFACT_DIR, project_name));
 
-    fn environment_variables(
-        &self,
-        _app: &App,
-        _env: &Environment,
-    ) -> Result<Option<EnvironmentVariables>> {
-        let env_vars = EnvironmentVariables::from([
+        let mut plan = BuildPlan::new(vec![setup, install, build], Some(start));
+        plan.add_variables(EnvironmentVariables::from([
             (
                 "ASPNETCORE_ENVIRONMENT".to_string(),
                 "Production".to_string(),
@@ -68,7 +53,8 @@ impl Provider for FSharpProvider {
                 "DOTNET_ROOT".to_string(),
                 "/nix/var/nix/profiles/default/".to_string(),
             ),
-        ]);
-        Ok(Some(env_vars))
+        ]));
+
+        Ok(Some(plan))
     }
 }
diff --git a/src/providers/haskell.rs b/src/providers/haskell.rs
index 5d795ca..ea26aac 100644
--- a/src/providers/haskell.rs
+++ b/src/providers/haskell.rs
@@ -3,8 +3,9 @@ use crate::nixpacks::{
     app::App,
     environment::Environment,
     nix::pkg::Pkg,
-    plan::legacy_phase::{
-        LegacyBuildPhase, LegacyInstallPhase, LegacySetupPhase, LegacyStartPhase,
+    plan::{
+        phase::{Phase, StartPhase},
+        BuildPlan,
     },
 };
 use anyhow::Result;
@@ -26,9 +27,9 @@ impl Provider for HaskellStackProvider {
         Ok(app.includes_file("package.yaml") && app.has_match("**/*.hs"))
     }
 
-    fn setup(&self, _app: &App, _env: &Environment) -> Result<Option<LegacySetupPhase>> {
-        let mut setup_phase = LegacySetupPhase::new(vec![Pkg::new("stack")]);
-        setup_phase.add_apt_pkgs(vec![
+    fn get_build_plan(&self, app: &App, _env: &Environment) -> Result<Option<BuildPlan>> {
+        let mut setup = Phase::setup(Some(vec![Pkg::new("stack")]));
+        setup.add_apt_pkgs(vec![
             "libgmp-dev".to_string(),
             "gcc".to_string(),
             "binutils".to_string(),
@@ -36,7 +37,7 @@ impl Provider for HaskellStackProvider {
             "zlib1g-dev".to_string(),
         ]);
         if ARCH == "aarch64" {
-            setup_phase.add_apt_pkgs(vec![
+            setup.add_apt_pkgs(vec![
                 "libnuma1".to_string(),
                 "libnuma-dev".to_string(),
                 "libtinfo-dev".to_string(),
@@ -50,25 +51,13 @@ impl Provider for HaskellStackProvider {
             ]);
         }
 
-        Ok(Some(setup_phase))
-    }
-
-    fn install(&self, _app: &App, _env: &Environment) -> Result<Option<LegacyInstallPhase>> {
-        let mut install_phase = LegacyInstallPhase::new("stack setup".to_string());
-        install_phase.add_cache_directory(STACK_CACHE_DIR.to_string());
-
-        Ok(Some(install_phase))
-    }
+        let mut install = Phase::install(Some("stack setup".to_string()));
+        install.add_cache_directory(STACK_CACHE_DIR.to_string());
 
-    fn build(&self, _app: &App, _env: &Environment) -> Result<Option<LegacyBuildPhase>> {
-        let mut build_phase = LegacyBuildPhase::new("stack install".to_string());
-        build_phase.add_cache_directory(STACK_CACHE_DIR.to_string());
-        build_phase.add_cache_directory(STACK_WORK_CACHE_DIR.to_string());
+        let mut build = Phase::build(Some("stack install".to_string()));
+        build.add_cache_directory(STACK_CACHE_DIR.to_string());
+        build.add_cache_directory(STACK_WORK_CACHE_DIR.to_string());
 
-        Ok(Some(build_phase))
-    }
-
-    fn start(&self, app: &App, _env: &Environment) -> Result<Option<LegacyStartPhase>> {
         let package: HaskellStackPackageYaml = app.read_yaml("package.yaml")?;
         let exe_names: Vec<String> = package.executables.keys().cloned().collect();
 
@@ -76,10 +65,11 @@ impl Provider for HaskellStackProvider {
             .get(0)
             .ok_or_else(|| anyhow::anyhow!("Failed to get executable name"))?;
 
-        Ok(Some(LegacyStartPhase::new(format!(
-            "/root/.local/bin/{}",
-            name
-        ))))
+        let start = StartPhase::new(format!("/root/.local/bin/{}", name));
+
+        let plan = BuildPlan::new(vec![setup, install, build], Some(start));
+
+        Ok(Some(plan))
     }
 }
 
diff --git a/src/providers/java.rs b/src/providers/java.rs
index 9adb463..9bbef14 100644
--- a/src/providers/java.rs
+++ b/src/providers/java.rs
@@ -3,7 +3,10 @@ use crate::nixpacks::{
     app::App,
     environment::Environment,
     nix::pkg::Pkg,
-    plan::legacy_phase::{LegacyBuildPhase, LegacySetupPhase, LegacyStartPhase},
+    plan::{
+        phase::{Phase, StartPhase},
+        BuildPlan,
+    },
 };
 use anyhow::Result;
 pub struct JavaProvider {}
@@ -24,23 +27,18 @@ impl Provider for JavaProvider {
             || app.includes_file("pom.yml"))
     }
 
-    fn setup(&self, _app: &App, _env: &Environment) -> Result<Option<LegacySetupPhase>> {
-        Ok(Some(LegacySetupPhase::new(vec![
-            Pkg::new("maven"),
-            Pkg::new("jdk8"),
-        ])))
-    }
+    fn get_build_plan(&self, app: &App, _env: &Environment) -> Result<Option<BuildPlan>> {
+        let setup = Phase::setup(Some(vec![Pkg::new("maven"), Pkg::new("jdk8")]));
 
-    fn build(&self, app: &App, _env: &Environment) -> Result<Option<LegacyBuildPhase>> {
         let mvn_exe = self.get_maven_exe(app);
-        Ok(Some(LegacyBuildPhase::new(format!("{mvn_exe} -DoutputFile=target/mvn-dependency-list.log -B -DskipTests clean dependency:list install", 
+        let build = Phase::build(Some(format!("{mvn_exe} -DoutputFile=target/mvn-dependency-list.log -B -DskipTests clean dependency:list install", 
             mvn_exe=mvn_exe
-        ))))
-    }
+        )));
+
+        let start = StartPhase::new(self.get_start_cmd(app));
 
-    fn start(&self, app: &App, _env: &Environment) -> Result<Option<LegacyStartPhase>> {
-        let start_cmd = self.get_start_cmd(app);
-        Ok(Some(LegacyStartPhase::new(start_cmd)))
+        let plan = BuildPlan::new(vec![setup, build], Some(start));
+        Ok(Some(plan))
     }
 }
 
diff --git a/src/providers/mod.rs b/src/providers/mod.rs
index 02eeed1..5ff5cae 100644
--- a/src/providers/mod.rs
+++ b/src/providers/mod.rs
@@ -1,11 +1,4 @@
-use crate::nixpacks::{
-    app::{App, StaticAssets},
-    environment::{Environment, EnvironmentVariables},
-    plan::{
-        legacy_phase::{LegacyBuildPhase, LegacyInstallPhase, LegacySetupPhase, LegacyStartPhase},
-        BuildPlan,
-    },
-};
+use crate::nixpacks::{app::App, environment::Environment, plan::BuildPlan};
 use anyhow::Result;
 
 pub mod clojure;
@@ -28,33 +21,6 @@ pub mod zig;
 
 pub trait Provider {
     fn name(&self) -> &str;
-
     fn detect(&self, app: &App, _env: &Environment) -> Result<bool>;
-
-    fn get_build_plan(&self, _app: &App, _environment: &Environment) -> Result<Option<BuildPlan>> {
-        Ok(None)
-    }
-
-    fn setup(&self, _app: &App, _env: &Environment) -> Result<Option<LegacySetupPhase>> {
-        Ok(None)
-    }
-    fn install(&self, _app: &App, _env: &Environment) -> Result<Option<LegacyInstallPhase>> {
-        Ok(None)
-    }
-    fn build(&self, _app: &App, _env: &Environment) -> Result<Option<LegacyBuildPhase>> {
-        Ok(None)
-    }
-    fn start(&self, _app: &App, _env: &Environment) -> Result<Option<LegacyStartPhase>> {
-        Ok(None)
-    }
-    fn static_assets(&self, _app: &App, _env: &Environment) -> Result<Option<StaticAssets>> {
-        Ok(None)
-    }
-    fn environment_variables(
-        &self,
-        _app: &App,
-        _env: &Environment,
-    ) -> Result<Option<EnvironmentVariables>> {
-        Ok(None)
-    }
+    fn get_build_plan(&self, _app: &App, _environment: &Environment) -> Result<Option<BuildPlan>>;
 }
diff --git a/src/providers/php.rs b/src/providers/php.rs
index c1a366a..6325d9e 100644
--- a/src/providers/php.rs
+++ b/src/providers/php.rs
@@ -6,8 +6,9 @@ use crate::nixpacks::{
     app::{App, StaticAssets},
     environment::{Environment, EnvironmentVariables},
     nix::pkg::Pkg,
-    plan::legacy_phase::{
-        LegacyBuildPhase, LegacyInstallPhase, LegacySetupPhase, LegacyStartPhase,
+    plan::{
+        phase::{Phase, StartPhase},
+        BuildPlan,
     },
 };
 
@@ -27,8 +28,30 @@ impl Provider for PhpProvider {
         Ok(app.includes_file("composer.json") || app.includes_file("index.php"))
     }
 
-    fn setup(&self, app: &App, env: &Environment) -> Result<Option<LegacySetupPhase>> {
-        let php_pkg = match self.get_php_package(app) {
+    fn get_build_plan(&self, app: &App, env: &Environment) -> Result<Option<BuildPlan>> {
+        let setup = PhpProvider::get_setup(app, env)?;
+        let install = PhpProvider::get_install(app);
+        let build = PhpProvider::get_build(app);
+        let start = PhpProvider::get_start(app);
+
+        let mut plan = BuildPlan::new(
+            vec![Some(setup), Some(install), build]
+                .into_iter()
+                .flatten()
+                .collect::<Vec<_>>(),
+            Some(start),
+        );
+
+        plan.add_static_assets(PhpProvider::static_assets());
+        plan.add_variables(PhpProvider::environment_variables(app));
+
+        Ok(Some(plan))
+    }
+}
+
+impl PhpProvider {
+    fn get_setup(app: &App, env: &Environment) -> Result<Phase> {
+        let php_pkg = match PhpProvider::get_php_package(app) {
             Ok(php_package) => php_package,
             _ => "php".to_string(),
         };
@@ -38,7 +61,7 @@ impl Provider for PhpProvider {
             Pkg::new("nginx"),
             Pkg::new(&format!("{}Packages.composer", &php_pkg)),
         ];
-        if let Ok(php_extensions) = self.get_php_extensions(app) {
+        if let Ok(php_extensions) = PhpProvider::get_php_extensions(app) {
             for extension in php_extensions {
                 pkgs.push(Pkg::new(&format!("{}Extensions.{}", &php_pkg, extension)));
             }
@@ -48,72 +71,69 @@ impl Provider for PhpProvider {
             pkgs.append(&mut NodeProvider::get_nix_packages(app, env)?);
         }
 
-        Ok(Some(LegacySetupPhase::new(pkgs)))
+        Ok(Phase::setup(Some(pkgs)))
     }
 
-    fn install(&self, app: &App, _env: &Environment) -> Result<Option<LegacyInstallPhase>> {
-        let mut install_phase = LegacyInstallPhase::new(
+    fn get_install(app: &App) -> Phase {
+        let mut install = Phase::install(Some(
             "mkdir -p /var/log/nginx && mkdir -p /var/cache/nginx".to_string(),
-        );
+        ));
         if app.includes_file("composer.json") {
-            install_phase.add_cmd("composer install".to_string());
+            install.add_cmd("composer install".to_string());
         };
         if app.includes_file("package.json") {
-            install_phase.add_cmd(NodeProvider::get_install_command(app));
+            install.add_cmd(NodeProvider::get_install_command(app));
         }
-        Ok(Some(install_phase))
+
+        install
     }
 
-    fn build(&self, app: &App, _env: &Environment) -> Result<Option<LegacyBuildPhase>> {
+    fn get_build(app: &App) -> Option<Phase> {
         if let Ok(true) = NodeProvider::has_script(app, "prod") {
-            return Ok(Some(LegacyBuildPhase::new(
+            return Some(Phase::build(Some(
                 NodeProvider::get_package_manager(app) + " run prod",
             )));
         } else if let Ok(true) = NodeProvider::has_script(app, "build") {
-            return Ok(Some(LegacyBuildPhase::new(
+            return Some(Phase::build(Some(
                 NodeProvider::get_package_manager(app) + " run build",
             )));
         }
-        Ok(None)
+
+        None
     }
 
-    fn start(&self, app: &App, _env: &Environment) -> Result<Option<LegacyStartPhase>> {
-        Ok(Some(LegacyStartPhase::new(format!(
+    fn get_start(app: &App) -> StartPhase {
+        StartPhase::new(format!(
             "([ -e /app/storage ] && chmod -R ugo+w /app/storage); perl {} {} /nginx.conf && echo \"Server starting on port $PORT\" && (php-fpm -y {} & nginx -c /nginx.conf)",
             app.asset_path("transform-config.pl"),
             app.asset_path("nginx.template.conf"),
             app.asset_path("php-fpm.conf"),
-        ))))
+        ))
     }
 
-    fn static_assets(&self, _app: &App, _env: &Environment) -> Result<Option<StaticAssets>> {
-        Ok(Some(static_asset_list! {
+    fn static_assets() -> StaticAssets {
+        static_asset_list! {
             "nginx.template.conf" => include_str!("php/nginx.template.conf"),
             "transform-config.pl" => include_str!("php/transform-config.pl"),
             "php-fpm.conf" => include_str!("php/php-fpm.conf")
-        }))
+        }
     }
 
-    fn environment_variables(
-        &self,
-        app: &App,
-        _env: &Environment,
-    ) -> Result<Option<EnvironmentVariables>> {
+    fn environment_variables(app: &App) -> EnvironmentVariables {
         let mut vars = EnvironmentVariables::new();
         vars.insert("PORT".to_string(), "80".to_string());
         if app.includes_file("artisan") {
             vars.insert("IS_LARAVEL".to_string(), "yes".to_string());
         }
-        Ok(Some(vars))
+        vars
     }
-}
 
-impl PhpProvider {
-    fn get_php_package(&self, app: &App) -> Result<String> {
-        let version = self.get_php_version(app)?;
+    fn get_php_package(app: &App) -> Result<String> {
+        let version = PhpProvider::get_php_version(app)?;
         Ok(format!("php{}", version.replace('.', "")))
     }
-    fn get_php_version(&self, app: &App) -> Result<String> {
+
+    fn get_php_version(app: &App) -> Result<String> {
         let composer_json: ComposerJson = app.read_json("composer.json")?;
         let version = composer_json.require.get("php").cloned();
         Ok(match version {
@@ -138,7 +158,8 @@ impl PhpProvider {
             }
         })
     }
-    fn get_php_extensions(&self, app: &App) -> Result<Vec<String>> {
+
+    fn get_php_extensions(app: &App) -> Result<Vec<String>> {
         let composer_json: ComposerJson = app.read_json("composer.json")?;
         let mut extensions = Vec::new();
         for extension in composer_json.require.keys() {
diff --git a/src/providers/python.rs b/src/providers/python.rs
index ca831e7..e085ecd 100644
--- a/src/providers/python.rs
+++ b/src/providers/python.rs
@@ -57,20 +57,6 @@ impl Provider for PythonProvider {
 
         Ok(Some(plan))
     }
-
-    fn environment_variables(
-        &self,
-        app: &App,
-        _env: &Environment,
-    ) -> Result<Option<EnvironmentVariables>> {
-        if app.includes_file("poetry.lock") {
-            return Ok(Some(EnvironmentVariables::from([(
-                "NIXPACKS_POETRY_VERSION".to_string(),
-                POETRY_VERSION.to_string(),
-            )])));
-        }
-        Ok(None)
-    }
 }
 
 #[derive(Debug, Deserialize, Clone)]
diff --git a/src/providers/ruby.rs b/src/providers/ruby.rs
index 66a52d5..facc2e2 100644
--- a/src/providers/ruby.rs
+++ b/src/providers/ruby.rs
@@ -2,8 +2,9 @@ use super::{node::NodeProvider, Provider};
 use crate::nixpacks::{
     app::App,
     environment::{Environment, EnvironmentVariables},
-    plan::legacy_phase::{
-        LegacyBuildPhase, LegacyInstallPhase, LegacySetupPhase, LegacyStartPhase,
+    plan::{
+        phase::{Phase, StartPhase},
+        BuildPlan,
     },
 };
 use anyhow::{bail, Ok, Result};
@@ -22,47 +23,68 @@ impl Provider for RubyProvider {
         Ok(app.includes_file("Gemfile"))
     }
 
-    fn setup(&self, app: &App, env: &Environment) -> Result<Option<LegacySetupPhase>> {
+    fn get_build_plan(&self, app: &App, env: &Environment) -> Result<Option<BuildPlan>> {
+        let setup = self.get_setup(app, env)?;
+        let install = self.get_install(app)?;
+        let build = self.get_build(app)?;
+        let start = self.get_start(app)?;
+
+        let mut plan = BuildPlan::new(
+            vec![setup, install, build]
+                .into_iter()
+                .flatten()
+                .collect::<Vec<_>>(),
+            start,
+        );
+
+        plan.add_variables(self.get_environment_variables(app)?);
+
+        Ok(Some(plan))
+    }
+}
+
+impl RubyProvider {
+    fn get_setup(&self, app: &App, env: &Environment) -> Result<Option<Phase>> {
         let mut pkgs = vec![];
         if app.includes_file("package.json") {
             pkgs = NodeProvider::get_nix_packages(app, env)?;
         }
-        let mut setup_phase = LegacySetupPhase::new(pkgs);
-        setup_phase.add_apt_pkgs(vec!["procps".to_string()]);
+
+        let mut setup = Phase::setup(Some(pkgs));
+        setup.add_apt_pkgs(vec!["procps".to_string()]);
 
         if self.uses_postgres(app)? {
-            setup_phase.add_apt_pkgs(vec!["libpq-dev".to_string()]);
+            setup.add_apt_pkgs(vec!["libpq-dev".to_string()]);
         }
 
-        setup_phase.add_cmd(
+        setup.add_cmd(
             "curl -sSL https://get.rvm.io | bash -s stable && . /etc/profile.d/rvm.sh".to_string(),
         );
 
-        setup_phase.add_cmd(format!("rvm install {}", self.get_ruby_version(app)?));
-        setup_phase.add_cmd(format!("rvm --default use {}", self.get_ruby_version(app)?));
-        setup_phase.add_cmd(format!("gem install {}", self.get_bundler_version(app)));
-        setup_phase
-            .add_cmd("echo 'source /usr/local/rvm/scripts/rvm' >> /root/.profile".to_string());
+        setup.add_cmd(format!("rvm install {}", self.get_ruby_version(app)?));
+        setup.add_cmd(format!("rvm --default use {}", self.get_ruby_version(app)?));
+        setup.add_cmd(format!("gem install {}", self.get_bundler_version(app)));
+        setup.add_cmd("echo 'source /usr/local/rvm/scripts/rvm' >> /root/.profile".to_string());
 
-        Ok(Some(setup_phase))
+        Ok(Some(setup))
     }
 
-    fn install(&self, app: &App, _env: &Environment) -> Result<Option<LegacyInstallPhase>> {
-        let mut install_phase = LegacyInstallPhase::default();
-        install_phase.add_file_dependency("Gemfile*".to_string());
-        install_phase.add_cache_directory(BUNDLE_CACHE_DIR.to_string());
+    fn get_install(&self, app: &App) -> Result<Option<Phase>> {
+        let mut install = Phase::install(None);
+        install.add_file_dependency("Gemfile*".to_string());
+        install.add_cache_directory(BUNDLE_CACHE_DIR.to_string());
 
-        install_phase.add_cmd("bundle install".to_string());
+        install.add_cmd("bundle install".to_string());
 
         // Ensure that the ruby executable is in the PATH
         let ruby_version = self.get_ruby_version(app)?;
-        install_phase.add_path(format!("/usr/local/rvm/rubies/{}/bin", ruby_version));
-        install_phase.add_path(format!("/usr/local/rvm/gems/{}/bin", ruby_version));
-        install_phase.add_path(format!("/usr/local/rvm/gems/{}@global/bin", ruby_version));
+        install.add_path(format!("/usr/local/rvm/rubies/{}/bin", ruby_version));
+        install.add_path(format!("/usr/local/rvm/gems/{}/bin", ruby_version));
+        install.add_path(format!("/usr/local/rvm/gems/{}@global/bin", ruby_version));
 
         if app.includes_file("package.json") {
-            install_phase.add_file_dependency("package.json".to_string());
-            install_phase
+            install.add_file_dependency("package.json".to_string());
+            install
                 .cmds
                 .clone()
                 .unwrap_or_default()
@@ -70,39 +92,35 @@ impl Provider for RubyProvider {
 
             for file in ["package.json", "package-lock.json"] {
                 if app.includes_file(file) {
-                    install_phase.add_file_dependency(file.to_string());
+                    install.add_file_dependency(file.to_string());
                 }
             }
         }
-        Ok(Some(install_phase))
+
+        Ok(Some(install))
     }
 
-    fn build(&self, app: &App, _env: &Environment) -> Result<Option<LegacyBuildPhase>> {
+    fn get_build(&self, app: &App) -> Result<Option<Phase>> {
         if self.is_rails_app(app) {
-            Ok(Some(LegacyBuildPhase::new(
+            Ok(Some(Phase::build(Some(
                 "bundle exec rake assets:precompile".to_string(),
-            )))
+            ))))
         } else {
             Ok(None)
         }
     }
 
-    fn start(&self, app: &App, _env: &Environment) -> Result<Option<LegacyStartPhase>> {
+    fn get_start(&self, app: &App) -> Result<Option<StartPhase>> {
         if let Some(start_cmd) = self.get_start_command(app) {
-            let start_phase = LegacyStartPhase::new(start_cmd);
-            Ok(Some(start_phase))
+            Ok(Some(StartPhase::new(start_cmd)))
         } else {
             Ok(None)
         }
     }
 
-    fn environment_variables(
-        &self,
-        app: &App,
-        _env: &Environment,
-    ) -> Result<Option<crate::nixpacks::environment::EnvironmentVariables>> {
+    fn get_environment_variables(&self, app: &App) -> Result<EnvironmentVariables> {
         let ruby_version = self.get_ruby_version(app)?;
-        Ok(Some(EnvironmentVariables::from([
+        Ok(EnvironmentVariables::from([
             ("BUNDLE_GEMFILE".to_string(), "/app/Gemfile".to_string()),
             (
                 "GEM_PATH".to_string(),
@@ -115,11 +133,9 @@ impl Provider for RubyProvider {
                 "GEM_HOME".to_string(),
                 format!("/usr/local/rvm/gems/{}", ruby_version),
             ),
-        ])))
+        ]))
     }
-}
 
-impl RubyProvider {
     fn get_start_command(&self, app: &App) -> Option<String> {
         if self.is_rails_app(app) {
             if app.includes_file("rails") {
diff --git a/src/providers/staticfile.rs b/src/providers/staticfile.rs
index 6a15224..ee5c682 100644
--- a/src/providers/staticfile.rs
+++ b/src/providers/staticfile.rs
@@ -3,7 +3,10 @@ use crate::nixpacks::{
     app::{App, StaticAssets},
     environment::Environment,
     nix::pkg::Pkg,
-    plan::legacy_phase::{LegacyBuildPhase, LegacySetupPhase, LegacyStartPhase},
+    plan::{
+        phase::{Phase, StartPhase},
+        BuildPlan,
+    },
 };
 use anyhow::Result;
 use indoc::formatdoc;
@@ -34,12 +37,48 @@ impl Provider for StaticfileProvider {
             || app.includes_file("index.html"))
     }
 
-    fn setup(&self, _app: &App, _env: &Environment) -> Result<Option<LegacySetupPhase>> {
-        let pkg = Pkg::new("nginx");
-        Ok(Some(LegacySetupPhase::new(vec![pkg])))
+    fn get_build_plan(&self, app: &App, env: &Environment) -> Result<Option<BuildPlan>> {
+        let setup = Phase::setup(Some(vec![Pkg::new("nginx")]));
+        let build = Phase::build(Some(
+            "mkdir /etc/nginx/ /var/log/nginx/ /var/cache/nginx/".to_string(),
+        ));
+
+        // shell command to edit 0.0.0.0:80 to $PORT
+        let shell_cmd = "[[ -z \"${PORT}\" ]] && echo \"Environment variable PORT not found. Using PORT 80\" || sed -i \"s/0.0.0.0:80/$PORT/g\"";
+        let start = StartPhase::new(format!(
+            "{shell_cmd} {conf_location} && nginx -c {conf_location}",
+            shell_cmd = shell_cmd,
+            conf_location = app.asset_path("nginx.conf"),
+        ));
+
+        let static_assets = StaticfileProvider::get_static_assets(app, env)?;
+
+        let mut plan = BuildPlan::new(vec![setup, build], Some(start));
+        plan.add_static_assets(static_assets);
+
+        Ok(Some(plan))
+    }
+}
+
+impl StaticfileProvider {
+    pub fn get_root(app: &App, env: &Environment, staticfile_root: String) -> String {
+        let mut root = "".to_string();
+        if let Some(staticfile_root) = env.get_config_variable("STATICFILE_ROOT") {
+            root = staticfile_root;
+        } else if !staticfile_root.is_empty() {
+            root = staticfile_root;
+        } else if app.includes_directory("public") {
+            root = "public".to_string();
+        } else if app.includes_directory("dist") {
+            root = "dist".to_string();
+        } else if app.includes_directory("index") {
+            root = "index".to_string();
+        }
+
+        root
     }
 
-    fn static_assets(&self, app: &App, env: &Environment) -> Result<Option<StaticAssets>> {
+    fn get_static_assets(app: &App, env: &Environment) -> Result<StaticAssets> {
         let mut assets = StaticAssets::new();
 
         let mut mime_types = "include /nix/store/*-user-environment/conf/mime.types;".to_string();
@@ -106,41 +145,7 @@ impl Provider for StaticfileProvider {
         error_page = error_page
         };
         assets.insert("nginx.conf".to_string(), nginx_conf);
-        Ok(Some(assets))
-    }
-
-    fn build(&self, _app: &App, _env: &Environment) -> Result<Option<LegacyBuildPhase>> {
-        Ok(Some(LegacyBuildPhase::new(
-            "mkdir /etc/nginx/ /var/log/nginx/ /var/cache/nginx/".to_string(),
-        )))
-    }
-
-    fn start(&self, app: &App, _env: &Environment) -> Result<Option<LegacyStartPhase>> {
-        // shell command to edit 0.0.0.0:80 to $PORT
-        let shell_cmd = "[[ -z \"${PORT}\" ]] && echo \"Environment variable PORT not found. Using PORT 80\" || sed -i \"s/0.0.0.0:80/$PORT/g\"";
-        Ok(Some(LegacyStartPhase::new(format!(
-            "{shell_cmd} {conf_location} && nginx -c {conf_location}",
-            shell_cmd = shell_cmd,
-            conf_location = app.asset_path("nginx.conf"),
-        ))))
-    }
-}
-
-impl StaticfileProvider {
-    pub fn get_root(app: &App, env: &Environment, staticfile_root: String) -> String {
-        let mut root = "".to_string();
-        if let Some(staticfile_root) = env.get_config_variable("STATICFILE_ROOT") {
-            root = staticfile_root;
-        } else if !staticfile_root.is_empty() {
-            root = staticfile_root;
-        } else if app.includes_directory("public") {
-            root = "public".to_string();
-        } else if app.includes_directory("dist") {
-            root = "dist".to_string();
-        } else if app.includes_directory("index") {
-            root = "index".to_string();
-        }
 
-        root
+        Ok(assets)
     }
 }
diff --git a/src/providers/swift.rs b/src/providers/swift.rs
index be8610d..8a01b3d 100644
--- a/src/providers/swift.rs
+++ b/src/providers/swift.rs
@@ -3,8 +3,9 @@ use crate::nixpacks::{
     app::App,
     environment::Environment,
     nix::pkg::Pkg,
-    plan::legacy_phase::{
-        LegacyBuildPhase, LegacyInstallPhase, LegacySetupPhase, LegacyStartPhase,
+    plan::{
+        phase::{Phase, StartPhase},
+        BuildPlan,
     },
 };
 use anyhow::{bail, Result};
@@ -38,23 +39,25 @@ impl Provider for SwiftProvider {
         Ok(app.includes_file("Package.swift"))
     }
 
-    fn setup(&self, app: &App, _env: &Environment) -> Result<Option<LegacySetupPhase>> {
-        let mut setup_phase = LegacySetupPhase::new(vec![
+    fn get_build_plan(&self, app: &App, _env: &Environment) -> Result<Option<BuildPlan>> {
+        let _plan = BuildPlan::default();
+
+        let mut setup = Phase::setup(Some(vec![
             Pkg::new("coreutils"),
             Pkg::new("swift"),
             Pkg::new("clang"),
             Pkg::new("zlib"),
             Pkg::new("zlib.dev"),
-        ]);
+        ]));
 
         let swift_version = SwiftProvider::get_swift_version(app)?;
         let rev = SwiftProvider::version_number_to_rev(&swift_version);
 
         if let Some(rev) = rev {
-            setup_phase.set_archive(rev);
+            setup.set_nix_archive(rev);
         } else {
             // Safe to unwrap, "5.4.2" exists on `AVAILABLE_SWIFT_VERSIONS`
-            setup_phase.set_archive(
+            setup.set_nix_archive(
                 AVAILABLE_SWIFT_VERSIONS
                     .iter()
                     .find(|(ver, _rev)| *ver == DEFAULT_SWIFT_VERSION)
@@ -64,37 +67,27 @@ impl Provider for SwiftProvider {
             );
         }
 
-        Ok(Some(setup_phase))
-    }
-
-    fn install(&self, app: &App, _env: &Environment) -> Result<Option<LegacyInstallPhase>> {
-        let mut install_phase = LegacyInstallPhase::new("swift package resolve".to_string());
-
-        install_phase.add_file_dependency("Package.swift".to_string());
-
+        let mut install = Phase::install(Some("swift package resolve".to_string()));
+        install.add_file_dependency("Package.swift".to_string());
         if app.includes_file("Package.resolved") {
-            install_phase.add_file_dependency("Package.resolved".to_string());
+            install.add_file_dependency("Package.resolved".to_string());
         }
 
-        Ok(Some(install_phase))
-    }
-
-    fn build(&self, app: &App, _env: &Environment) -> Result<Option<LegacyBuildPhase>> {
         let name = SwiftProvider::get_executable_name(app)?;
-        let mut build_phase = LegacyBuildPhase::new(
+        let mut build = Phase::build(Some(
             "CC=clang++ swift build -c release --static-swift-stdlib".to_string(),
-        );
-        build_phase.add_cmd(format!(
+        ));
+        build.add_cmd(format!(
             "cp ./.build/release/{name} ./{name} && rm -rf ./.build",
             name = name
         ));
-        Ok(Some(build_phase))
-    }
 
-    fn start(&self, app: &App, _env: &Environment) -> Result<Option<LegacyStartPhase>> {
         let name = SwiftProvider::get_executable_name(app)?;
+        let start = StartPhase::new(format!("./{}", name));
+
+        let plan = BuildPlan::new(vec![setup, install, build], Some(start));
 
-        Ok(Some(LegacyStartPhase::new(format!("./{}", name))))
+        Ok(Some(plan))
     }
 }
 
diff --git a/src/providers/zig.rs b/src/providers/zig.rs
index 701529e..8be7cd2 100644
--- a/src/providers/zig.rs
+++ b/src/providers/zig.rs
@@ -3,8 +3,9 @@ use crate::nixpacks::{
     app::App,
     environment::Environment,
     nix::pkg::Pkg,
-    plan::legacy_phase::{
-        LegacyBuildPhase, LegacyInstallPhase, LegacySetupPhase, LegacyStartPhase,
+    plan::{
+        phase::{Phase, StartPhase},
+        BuildPlan,
     },
 };
 use anyhow::Result;
@@ -24,45 +25,39 @@ impl Provider for ZigProvider {
         Ok(app.has_match("*.zig") || app.has_match("**/*.zig") || app.has_match("gyro.zzz"))
     }
 
-    fn setup(&self, app: &App, _env: &Environment) -> Result<Option<LegacySetupPhase>> {
-        let mut pkgs = vec![Pkg::new("zig")];
+    fn get_build_plan(&self, app: &App, _env: &Environment) -> Result<Option<BuildPlan>> {
+        let mut setup = Phase::setup(Some(vec![Pkg::new("zig")]));
+
         if app.includes_file("gyro.zzz") {
-            pkgs.push(Pkg::new("wget"));
+            setup.add_nix_pkgs(vec![Pkg::new("wget")]);
         }
-        Ok(Some(LegacySetupPhase::new(pkgs)))
-    }
 
-    fn install(&self, app: &App, _env: &Environment) -> Result<Option<LegacyInstallPhase>> {
-        let mut phase = LegacyInstallPhase::default();
+        let mut install = Phase::install(None);
         if app.includes_file(".gitmodules") {
-            phase.add_cmd("git submodule update --init".to_string());
+            install.add_cmd("git submodule update --init".to_string());
         }
         if app.includes_file("gyro.zzz") {
             let gyro_exe_path = format!("/gyro/gyro-{}-linux-{}/bin/gyro", GYRO_VERSION, ARCH);
-            phase.add_cmd(format!(
+            install.add_cmd(format!(
                 "mkdir /gyro && (wget -O- {} | tar -C /gyro -xzf -)",
                 ZigProvider::get_gyro_download_url()
             ));
-            phase.add_cmd(format!("chmod +x {}", gyro_exe_path));
-            phase.add_cmd(format!("{} fetch", gyro_exe_path));
+            install.add_cmd(format!("chmod +x {}", gyro_exe_path));
+            install.add_cmd(format!("{} fetch", gyro_exe_path));
         }
-        Ok(Some(phase))
-    }
 
-    fn build(&self, _app: &App, _env: &Environment) -> Result<Option<LegacyBuildPhase>> {
-        Ok(Some(LegacyBuildPhase::new(
-            "zig build -Drelease-safe=true".to_string(),
-        )))
-    }
+        let build = Phase::build(Some("zig build -Drelease-safe=true".to_string()));
 
-    fn start(&self, app: &App, _env: &Environment) -> Result<Option<LegacyStartPhase>> {
-        Ok(Some(LegacyStartPhase::new(format!(
+        let start = StartPhase::new(format!(
             "./zig-out/bin/{}",
             app.source
                 .file_name()
                 .map(OsStr::to_str)
                 .map_or("*", Option::unwrap)
-        ))))
+        ));
+
+        let plan = BuildPlan::new(vec![setup, install, build], Some(start));
+        Ok(Some(plan))
     }
 }
 


Test patch:
diff --git a/tests/snapshots/generate_plan_tests__config_from_environment_variables.snap b/tests/snapshots/generate_plan_tests__config_from_environment_variables.snap
index 37ad85a..e305bc1 100644
--- a/tests/snapshots/generate_plan_tests__config_from_environment_variables.snap
+++ b/tests/snapshots/generate_plan_tests__config_from_environment_variables.snap
@@ -14,7 +14,6 @@ expression: plan
     "NIXPACKS_RUN_IMAGE": "alpine",
     "NIXPACKS_START_CMD": "start"
   },
-  "staticAssets": {},
   "phases": [
     {
       "name": "setup",
@@ -25,9 +24,7 @@ expression: plan
         {
           "name": "ripgrep"
         }
-      ],
-      "nixLibraries": [],
-      "aptPackages": []
+      ]
     },
     {
       "name": "install",
@@ -36,10 +33,6 @@ expression: plan
       ],
       "commands": [
         "install"
-      ],
-      "cacheDirectories": [
-        "/tmp",
-        "foobar"
       ]
     },
     {
@@ -49,15 +42,10 @@ expression: plan
       ],
       "commands": [
         "build"
-      ],
-      "cacheDirectories": [
-        "/build",
-        "barbaz"
       ]
     }
   ],
   "startPhase": {
-    "cmd": "start",
-    "runImage": "alpine"
+    "cmd": "start"
   }
 }
diff --git a/tests/snapshots/generate_plan_tests__csharp_api.snap b/tests/snapshots/generate_plan_tests__csharp_api.snap
index 6075347..266336e 100644
--- a/tests/snapshots/generate_plan_tests__csharp_api.snap
+++ b/tests/snapshots/generate_plan_tests__csharp_api.snap
@@ -10,18 +10,7 @@ expression: plan
     "ASPNETCORE_URLS": "http://0.0.0.0:3000",
     "DOTNET_ROOT": "/nix/var/nix/profiles/default/"
   },
-  "staticAssets": {},
   "phases": [
-    {
-      "name": "setup",
-      "nixPackages": [
-        {
-          "name": "dotnet-sdk"
-        }
-      ],
-      "nixLibraries": [],
-      "aptPackages": []
-    },
     {
       "name": "install",
       "dependsOn": [
@@ -31,6 +20,14 @@ expression: plan
         "dotnet restore"
       ]
     },
+    {
+      "name": "setup",
+      "nixPackages": [
+        {
+          "name": "dotnet-sdk"
+        }
+      ]
+    },
     {
       "name": "build",
       "dependsOn": [
diff --git a/tests/snapshots/generate_plan_tests__csharp_cli.snap b/tests/snapshots/generate_plan_tests__csharp_cli.snap
index a877ae5..51b4f1a 100644
--- a/tests/snapshots/generate_plan_tests__csharp_cli.snap
+++ b/tests/snapshots/generate_plan_tests__csharp_cli.snap
@@ -10,18 +10,7 @@ expression: plan
     "ASPNETCORE_URLS": "http://0.0.0.0:3000",
     "DOTNET_ROOT": "/nix/var/nix/profiles/default/"
   },
-  "staticAssets": {},
   "phases": [
-    {
-      "name": "setup",
-      "nixPackages": [
-        {
-          "name": "dotnet-sdk"
-        }
-      ],
-      "nixLibraries": [],
-      "aptPackages": []
-    },
     {
       "name": "install",
       "dependsOn": [
@@ -31,6 +20,14 @@ expression: plan
         "dotnet restore"
       ]
     },
+    {
+      "name": "setup",
+      "nixPackages": [
+        {
+          "name": "dotnet-sdk"
+        }
+      ]
+    },
     {
       "name": "build",
       "dependsOn": [
diff --git a/tests/snapshots/generate_plan_tests__custom_pkgs.snap b/tests/snapshots/generate_plan_tests__custom_pkgs.snap
index a15750b..b162487 100644
--- a/tests/snapshots/generate_plan_tests__custom_pkgs.snap
+++ b/tests/snapshots/generate_plan_tests__custom_pkgs.snap
@@ -5,8 +5,6 @@ expression: plan
 {
   "nixpacksVersion": "[version]",
   "buildImage": "[build_image]",
-  "variables": {},
-  "staticAssets": {},
   "phases": [
     {
       "name": "setup",
@@ -14,9 +12,7 @@ expression: plan
         {
           "name": "cowsay"
         }
-      ],
-      "nixLibraries": [],
-      "aptPackages": []
+      ]
     },
     {
       "name": "install",
diff --git a/tests/snapshots/generate_plan_tests__fsharp_api.snap b/tests/snapshots/generate_plan_tests__fsharp_api.snap
index b9990be..a4b251d 100644
--- a/tests/snapshots/generate_plan_tests__fsharp_api.snap
+++ b/tests/snapshots/generate_plan_tests__fsharp_api.snap
@@ -10,18 +10,7 @@ expression: plan
     "ASPNETCORE_URLS": "http://0.0.0.0:3000",
     "DOTNET_ROOT": "/nix/var/nix/profiles/default/"
   },
-  "staticAssets": {},
   "phases": [
-    {
-      "name": "setup",
-      "nixPackages": [
-        {
-          "name": "dotnet-sdk"
-        }
-      ],
-      "nixLibraries": [],
-      "aptPackages": []
-    },
     {
       "name": "install",
       "dependsOn": [
@@ -31,6 +20,14 @@ expression: plan
         "dotnet restore"
       ]
     },
+    {
+      "name": "setup",
+      "nixPackages": [
+        {
+          "name": "dotnet-sdk"
+        }
+      ]
+    },
     {
       "name": "build",
       "dependsOn": [
diff --git a/tests/snapshots/generate_plan_tests__fsharp_cli.snap b/tests/snapshots/generate_plan_tests__fsharp_cli.snap
index ee4f4e0..667d65b 100644
--- a/tests/snapshots/generate_plan_tests__fsharp_cli.snap
+++ b/tests/snapshots/generate_plan_tests__fsharp_cli.snap
@@ -10,18 +10,7 @@ expression: plan
     "ASPNETCORE_URLS": "http://0.0.0.0:3000",
     "DOTNET_ROOT": "/nix/var/nix/profiles/default/"
   },
-  "staticAssets": {},
   "phases": [
-    {
-      "name": "setup",
-      "nixPackages": [
-        {
-          "name": "dotnet-sdk"
-        }
-      ],
-      "nixLibraries": [],
-      "aptPackages": []
-    },
     {
       "name": "install",
       "dependsOn": [
@@ -31,6 +20,14 @@ expression: plan
         "dotnet restore"
       ]
     },
+    {
+      "name": "setup",
+      "nixPackages": [
+        {
+          "name": "dotnet-sdk"
+        }
+      ]
+    },
     {
       "name": "build",
       "dependsOn": [
diff --git a/tests/snapshots/generate_plan_tests__java_maven.snap b/tests/snapshots/generate_plan_tests__java_maven.snap
index 9c108f4..790133f 100644
--- a/tests/snapshots/generate_plan_tests__java_maven.snap
+++ b/tests/snapshots/generate_plan_tests__java_maven.snap
@@ -5,9 +5,13 @@ expression: plan
 {
   "nixpacksVersion": "[version]",
   "buildImage": "[build_image]",
-  "variables": {},
-  "staticAssets": {},
   "phases": [
+    {
+      "name": "install",
+      "dependsOn": [
+        "setup"
+      ]
+    },
     {
       "name": "setup",
       "nixPackages": [
@@ -17,14 +21,6 @@ expression: plan
         {
           "name": "jdk8"
         }
-      ],
-      "nixLibraries": [],
-      "aptPackages": []
-    },
-    {
-      "name": "install",
-      "dependsOn": [
-        "setup"
       ]
     },
     {
diff --git a/tests/snapshots/generate_plan_tests__java_maven_wrapper.snap b/tests/snapshots/generate_plan_tests__java_maven_wrapper.snap
index b6ac6b6..9a3ec4f 100644
--- a/tests/snapshots/generate_plan_tests__java_maven_wrapper.snap
+++ b/tests/snapshots/generate_plan_tests__java_maven_wrapper.snap
@@ -5,9 +5,13 @@ expression: plan
 {
   "nixpacksVersion": "[version]",
   "buildImage": "[build_image]",
-  "variables": {},
-  "staticAssets": {},
   "phases": [
+    {
+      "name": "install",
+      "dependsOn": [
+        "setup"
+      ]
+    },
     {
       "name": "setup",
       "nixPackages": [
@@ -17,14 +21,6 @@ expression: plan
         {
           "name": "jdk8"
         }
-      ],
-      "nixLibraries": [],
-      "aptPackages": []
-    },
-    {
-      "name": "install",
-      "dependsOn": [
-        "setup"
       ]
     },
     {
diff --git a/tests/snapshots/generate_plan_tests__php_laravel.snap b/tests/snapshots/generate_plan_tests__php_laravel.snap
index 4b76d0e..c3af8fc 100644
--- a/tests/snapshots/generate_plan_tests__php_laravel.snap
+++ b/tests/snapshots/generate_plan_tests__php_laravel.snap
@@ -15,6 +15,17 @@ expression: plan
     "transform-config.pl": "#!/usr/bin/env perl\n\nundef $/;\n\nsub if_stmt {\n    my $condition = $_[0];\n    my $value = $_[1];\n    my $else = $_[2];\n\n    if($ENV{$condition} eq \"yes\") {\n        return $value;\n    } else {\n        return $else;\n    }\n}\n\nsub get_nix_path {\n    my $exe = $_[0];\n    my $path = `which $exe`;\n    $path =~ s/\\n//;\n    my $storePath = `nix-store -q $path`;\n    $storePath =~ s/\\n//;\n    return $storePath;\n}\n\nif ($#ARGV != 1) {\n    print STDERR \"Usage: $0 <config-file> <output-file>\\n\";\n    exit 1;\n}\nmy $infile = $ARGV[0];\nmy $outfile = $ARGV[1];\nopen(FH, '<', $infile) or die \"Could not open configuration file '$infile' $!\";\nmy $out = '';\nwhile (<FH>) {\n\n    # If statements\n    s{\n        \\$if\\s*\\((\\w+)\\)\\s*\\(\n            ([\\s\\S]*?)\n        \\)\\s*else\\s*\\(\n            ([\\s\\S]*?)\n        \\)\n    }{if_stmt($1, $2, $3)}egx;\n\n    # Variables\n    s/\\$\\{(\\w+)\\}/$ENV{$1}/eg;\n    \n    # Nix paths\n    s/\\$\\!\\{(\\w+)\\}/get_nix_path($1)/eg;\n\n    $out .= $_;\n}\nclose(FH);\nopen(FH, '>', $outfile) or die \"Could not write configuration file '$outfile' $!\";\nprint FH $out;\nclose(FH);"
   },
   "phases": [
+    {
+      "name": "install",
+      "dependsOn": [
+        "setup"
+      ],
+      "commands": [
+        "mkdir -p /var/log/nginx && mkdir -p /var/cache/nginx",
+        "composer install",
+        "npm i"
+      ]
+    },
     {
       "name": "setup",
       "nixPackages": [
@@ -37,19 +48,6 @@ expression: plan
           "name": "npm-8_x",
           "overlay": "https://github.com/railwayapp/nix-npm-overlay/archive/main.tar.gz"
         }
-      ],
-      "nixLibraries": [],
-      "aptPackages": []
-    },
-    {
-      "name": "install",
-      "dependsOn": [
-        "setup"
-      ],
-      "commands": [
-        "mkdir -p /var/log/nginx && mkdir -p /var/cache/nginx",
-        "composer install",
-        "npm i"
       ]
     },
     {
diff --git a/tests/snapshots/generate_plan_tests__php_vanilla.snap b/tests/snapshots/generate_plan_tests__php_vanilla.snap
index 5b606a5..1f4ef0e 100644
--- a/tests/snapshots/generate_plan_tests__php_vanilla.snap
+++ b/tests/snapshots/generate_plan_tests__php_vanilla.snap
@@ -29,9 +29,7 @@ expression: plan
         {
           "name": "phpPackages.composer"
         }
-      ],
-      "nixLibraries": [],
-      "aptPackages": []
+      ]
     },
     {
       "name": "install",
diff --git a/tests/snapshots/generate_plan_tests__pin_archive.snap b/tests/snapshots/generate_plan_tests__pin_archive.snap
index b2abadf..0989c9c 100644
--- a/tests/snapshots/generate_plan_tests__pin_archive.snap
+++ b/tests/snapshots/generate_plan_tests__pin_archive.snap
@@ -5,15 +5,10 @@ expression: plan
 {
   "nixpacksVersion": "[version]",
   "buildImage": "[build_image]",
-  "variables": {},
-  "staticAssets": {},
   "phases": [
     {
       "name": "setup",
-      "nixPackages": [],
-      "nixLibraries": [],
-      "nixpacksArchive": "[archive]",
-      "aptPackages": []
+      "nixpacksArchive": "[archive]"
     },
     {
       "name": "install",
diff --git a/tests/snapshots/generate_plan_tests__ruby.snap b/tests/snapshots/generate_plan_tests__ruby.snap
index 016872e..82a3687 100644
--- a/tests/snapshots/generate_plan_tests__ruby.snap
+++ b/tests/snapshots/generate_plan_tests__ruby.snap
@@ -10,12 +10,9 @@ expression: plan
     "GEM_HOME": "/usr/local/rvm/gems/ruby-3.1.2",
     "GEM_PATH": "/usr/local/rvm/gems/ruby-3.1.2:/usr/local/rvm/gems/ruby-3.1.2@global"
   },
-  "staticAssets": {},
   "phases": [
     {
       "name": "setup",
-      "nixPackages": [],
-      "nixLibraries": [],
       "aptPackages": [
         "procps"
       ],
@@ -54,7 +51,5 @@ expression: plan
       ]
     }
   ],
-  "startPhase": {
-    "cmd": "ruby app.rb"
-  }
+  "startPhase": {}
 }
diff --git a/tests/snapshots/generate_plan_tests__ruby_rails_postgres.snap b/tests/snapshots/generate_plan_tests__ruby_rails_postgres.snap
index 90e167b..04c5848 100644
--- a/tests/snapshots/generate_plan_tests__ruby_rails_postgres.snap
+++ b/tests/snapshots/generate_plan_tests__ruby_rails_postgres.snap
@@ -10,24 +10,7 @@ expression: plan
     "GEM_HOME": "/usr/local/rvm/gems/3.1.2",
     "GEM_PATH": "/usr/local/rvm/gems/3.1.2:/usr/local/rvm/gems/3.1.2@global"
   },
-  "staticAssets": {},
   "phases": [
-    {
-      "name": "setup",
-      "nixPackages": [],
-      "nixLibraries": [],
-      "aptPackages": [
-        "procps",
-        "libpq-dev"
-      ],
-      "commands": [
-        "curl -sSL https://get.rvm.io | bash -s stable && . /etc/profile.d/rvm.sh",
-        "rvm install 3.1.2",
-        "rvm --default use 3.1.2",
-        "gem install bundler:2.3.7",
-        "echo 'source /usr/local/rvm/scripts/rvm' >> /root/.profile"
-      ]
-    },
     {
       "name": "install",
       "dependsOn": [
@@ -48,6 +31,20 @@ expression: plan
         "/usr/local/rvm/gems/3.1.2@global/bin"
       ]
     },
+    {
+      "name": "setup",
+      "aptPackages": [
+        "procps",
+        "libpq-dev"
+      ],
+      "commands": [
+        "curl -sSL https://get.rvm.io | bash -s stable && . /etc/profile.d/rvm.sh",
+        "rvm install 3.1.2",
+        "rvm --default use 3.1.2",
+        "gem install bundler:2.3.7",
+        "echo 'source /usr/local/rvm/scripts/rvm' >> /root/.profile"
+      ]
+    },
     {
       "name": "build",
       "dependsOn": [
diff --git a/tests/snapshots/generate_plan_tests__ruby_sinatra.snap b/tests/snapshots/generate_plan_tests__ruby_sinatra.snap
index f7f5af1..4d662d3 100644
--- a/tests/snapshots/generate_plan_tests__ruby_sinatra.snap
+++ b/tests/snapshots/generate_plan_tests__ruby_sinatra.snap
@@ -10,12 +10,9 @@ expression: plan
     "GEM_HOME": "/usr/local/rvm/gems/ruby-3.1.2",
     "GEM_PATH": "/usr/local/rvm/gems/ruby-3.1.2:/usr/local/rvm/gems/ruby-3.1.2@global"
   },
-  "staticAssets": {},
   "phases": [
     {
       "name": "setup",
-      "nixPackages": [],
-      "nixLibraries": [],
       "aptPackages": [
         "procps"
       ],
diff --git a/tests/snapshots/generate_plan_tests__shell_hello.snap b/tests/snapshots/generate_plan_tests__shell_hello.snap
index 4b807bf..1cd6fb8 100644
--- a/tests/snapshots/generate_plan_tests__shell_hello.snap
+++ b/tests/snapshots/generate_plan_tests__shell_hello.snap
@@ -5,14 +5,9 @@ expression: plan
 {
   "nixpacksVersion": "[version]",
   "buildImage": "[build_image]",
-  "variables": {},
-  "staticAssets": {},
   "phases": [
     {
-      "name": "setup",
-      "nixPackages": [],
-      "nixLibraries": [],
-      "aptPackages": []
+      "name": "setup"
     },
     {
       "name": "install",
diff --git a/tests/snapshots/generate_plan_tests__staticfile.snap b/tests/snapshots/generate_plan_tests__staticfile.snap
index 8766ea5..2594d9f 100644
--- a/tests/snapshots/generate_plan_tests__staticfile.snap
+++ b/tests/snapshots/generate_plan_tests__staticfile.snap
@@ -5,26 +5,23 @@ expression: plan
 {
   "nixpacksVersion": "[version]",
   "buildImage": "[build_image]",
-  "variables": {},
   "staticAssets": {
     ".htpasswd": "# Username: nixpacks\n# Password: nixpacks\nnixpacks:$apr1$21i3ye0o$d1H1ePSg8mD2cO5O2A9dG.",
     "nginx.conf": "daemon off;\nerror_log /dev/stdout info;\nworker_processes  auto;\nevents {\n    worker_connections  1024;\n}\n\nhttp {\n    include /nix/store/*-user-environment/conf/mime.types;\n    access_log /dev/stdout;\n    default_type  application/octet-stream;\n    sendfile       on;\n    keepalive_timeout  60;\n    types_hash_max_size 4096;\n    server {\n        listen    0.0.0.0:80;\n        gzip  \t  on;\n        root\t  /app/site;\n        location / {\n            auth_basic\t\"Password Required\";\nauth_basic_user_file\t/assets/.htpasswd;\n            autoindex off;\n        }\n\n    }\n}\n"
   },
   "phases": [
+    {
+      "name": "install",
+      "dependsOn": [
+        "setup"
+      ]
+    },
     {
       "name": "setup",
       "nixPackages": [
         {
           "name": "nginx"
         }
-      ],
-      "nixLibraries": [],
-      "aptPackages": []
-    },
-    {
-      "name": "install",
-      "dependsOn": [
-        "setup"
       ]
     },
     {
diff --git a/tests/snapshots/generate_plan_tests__swift.snap b/tests/snapshots/generate_plan_tests__swift.snap
index ce8bc7a..fa0c534 100644
--- a/tests/snapshots/generate_plan_tests__swift.snap
+++ b/tests/snapshots/generate_plan_tests__swift.snap
@@ -5,9 +5,19 @@ expression: plan
 {
   "nixpacksVersion": "[version]",
   "buildImage": "[build_image]",
-  "variables": {},
-  "staticAssets": {},
   "phases": [
+    {
+      "name": "install",
+      "dependsOn": [
+        "setup"
+      ],
+      "commands": [
+        "swift package resolve"
+      ],
+      "onlyIncludeFiles": [
+        "Package.swift"
+      ]
+    },
     {
       "name": "setup",
       "nixPackages": [
@@ -27,21 +37,7 @@ expression: plan
           "name": "zlib.dev"
         }
       ],
-      "nixLibraries": [],
-      "nixpacksArchive": "[archive]",
-      "aptPackages": []
-    },
-    {
-      "name": "install",
-      "dependsOn": [
-        "setup"
-      ],
-      "commands": [
-        "swift package resolve"
-      ],
-      "onlyIncludeFiles": [
-        "Package.swift"
-      ]
+      "nixpacksArchive": "[archive]"
     },
     {
       "name": "build",
diff --git a/tests/snapshots/generate_plan_tests__swift_custom_version.snap b/tests/snapshots/generate_plan_tests__swift_custom_version.snap
index ce8bc7a..fa0c534 100644
--- a/tests/snapshots/generate_plan_tests__swift_custom_version.snap
+++ b/tests/snapshots/generate_plan_tests__swift_custom_version.snap
@@ -5,9 +5,19 @@ expression: plan
 {
   "nixpacksVersion": "[version]",
   "buildImage": "[build_image]",
-  "variables": {},
-  "staticAssets": {},
   "phases": [
+    {
+      "name": "install",
+      "dependsOn": [
+        "setup"
+      ],
+      "commands": [
+        "swift package resolve"
+      ],
+      "onlyIncludeFiles": [
+        "Package.swift"
+      ]
+    },
     {
       "name": "setup",
       "nixPackages": [
@@ -27,21 +37,7 @@ expression: plan
           "name": "zlib.dev"
         }
       ],
-      "nixLibraries": [],
-      "nixpacksArchive": "[archive]",
-      "aptPackages": []
-    },
-    {
-      "name": "install",
-      "dependsOn": [
-        "setup"
-      ],
-      "commands": [
-        "swift package resolve"
-      ],
-      "onlyIncludeFiles": [
-        "Package.swift"
-      ]
+      "nixpacksArchive": "[archive]"
     },
     {
       "name": "build",
diff --git a/tests/snapshots/generate_plan_tests__swift_vapor.snap b/tests/snapshots/generate_plan_tests__swift_vapor.snap
index 1925cfd..e1721d4 100644
--- a/tests/snapshots/generate_plan_tests__swift_vapor.snap
+++ b/tests/snapshots/generate_plan_tests__swift_vapor.snap
@@ -5,9 +5,20 @@ expression: plan
 {
   "nixpacksVersion": "[version]",
   "buildImage": "[build_image]",
-  "variables": {},
-  "staticAssets": {},
   "phases": [
+    {
+      "name": "install",
+      "dependsOn": [
+        "setup"
+      ],
+      "commands": [
+        "swift package resolve"
+      ],
+      "onlyIncludeFiles": [
+        "Package.swift",
+        "Package.resolved"
+      ]
+    },
     {
       "name": "setup",
       "nixPackages": [
@@ -27,22 +38,7 @@ expression: plan
           "name": "zlib.dev"
         }
       ],
-      "nixLibraries": [],
-      "nixpacksArchive": "[archive]",
-      "aptPackages": []
-    },
-    {
-      "name": "install",
-      "dependsOn": [
-        "setup"
-      ],
-      "commands": [
-        "swift package resolve"
-      ],
-      "onlyIncludeFiles": [
-        "Package.swift",
-        "Package.resolved"
-      ]
+      "nixpacksArchive": "[archive]"
     },
     {
       "name": "build",
diff --git a/tests/snapshots/generate_plan_tests__zig.snap b/tests/snapshots/generate_plan_tests__zig.snap
index 8f95861..dace784 100644
--- a/tests/snapshots/generate_plan_tests__zig.snap
+++ b/tests/snapshots/generate_plan_tests__zig.snap
@@ -5,23 +5,19 @@ expression: plan
 {
   "nixpacksVersion": "[version]",
   "buildImage": "[build_image]",
-  "variables": {},
-  "staticAssets": {},
   "phases": [
+    {
+      "name": "install",
+      "dependsOn": [
+        "setup"
+      ]
+    },
     {
       "name": "setup",
       "nixPackages": [
         {
           "name": "zig"
         }
-      ],
-      "nixLibraries": [],
-      "aptPackages": []
-    },
-    {
-      "name": "install",
-      "dependsOn": [
-        "setup"
       ]
     },
     {