diff --git a/.idea/scopes/NCC_Source_files.xml b/.idea/scopes/NCC_Source_files.xml new file mode 100644 index 0000000..6e38ebb --- /dev/null +++ b/.idea/scopes/NCC_Source_files.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/.idea/scopes/Third_Party_Source_Files.xml b/.idea/scopes/Third_Party_Source_Files.xml new file mode 100644 index 0000000..73f6682 --- /dev/null +++ b/.idea/scopes/Third_Party_Source_Files.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/assets/pkg_struct_1.0.png b/assets/pkg_struct_1.0.png new file mode 100644 index 0000000..31b102a Binary files /dev/null and b/assets/pkg_struct_1.0.png differ diff --git a/docs/project_configuration/execution_policy.md b/docs/project_configuration/execution_policy.md new file mode 100644 index 0000000..f5b851b --- /dev/null +++ b/docs/project_configuration/execution_policy.md @@ -0,0 +1,151 @@ +# Execution Policies + +**Updated on Sunday, November 20, 2022** + +An execution policy is a policy defined in the Project +configuration file (`project.json`) that can be used +to execute a script or program in any stage of the package + +For instance, you can have a script that is executed before +the build process starts, or in different installation stages +when the user is installing your package you can have a unit +run before or after the installation/uninstallation process +starts.# + +Use cases such as this allows you to properly implement +and control your program's files & assets that are not +handled by NCC's compiler extensions. + +## Table of Contents + + +* [Execution Policies](#execution-policies) + * [Table of Contents](#table-of-contents) + * [JSON Example](#json-example) + * [ExecutionPolicy Object](#executionpolicy-object) + * [Object Properties](#object-properties) + * [JSON Example](#json-example) + * [ExecutionConfiguration Object](#executionconfiguration-object) + * [Object Properties](#object-properties) + * [JSON Example](#json-example) + * [ExitHandler Object](#exithandler-object) + * [Object Properties](#object-properties) + * [JSON Example](#json-example) + + + +## JSON Example + +```json +{ + "execution_policies": { + "main": { + "runner": "php", + "message": "Running main %ASSEMBLY.PACKAGE%", + "exec": { + "target": "scripts/main.php", + "working_directory": "%INSTALL_PATH.SRC%", + "silent": false + } + }, + "hello_world": { + "runner": "shell", + "message": "Running HTOP", + "options": { + "htop": null + }, + "exec": { + "tty": true + } + } + } +} +``` + +------------------------------------------------------------ + +## ExecutionPolicy Object + +Execution Policies for your project **must** have unique +names, because they way you tell NCC to execute these +policies is by referencing their name in the configuration. + +Invalid names/undefined policies will raise errors when +building the project + +### Object Properties + +| Property Name | Value Type | Example Value | Description | +|-----------------|---------------------------------|----------------------|--------------------------------------------------------------------------------------------| +| `runner` | string | bash | The name of a supported runner instance, see runners in this document | +| `message` | string, null | Starting foo_bar ... | *Optional* the message to display before running the execution policy | +| `exec` | ExecutionConfiguration | N/A | The configuration object that tells how the runner should execute the process | +| `exit_handlers` | ExitHandlersConfiguration, null | N/A | *Optional* Exit Handler Configurations that tells NCC how to handle exits from the process | + +### JSON Example + +```json +{ + "name": "foo_bar", + "runner": "bash", + "message": "Running foo_bar ...", + "exec": null, + "exit_handlers": null +} +``` + +------------------------------------------------------------ + +## ExecutionConfiguration Object + +### Object Properties + +| Property Name | Value Type | Example Value | Description | +|---------------------|-------------------|---------------------------------|------------------------------------------------------------------------| +| `target` | `string` | scripts/foo_bar.bash | The target file to execute | +| `working_directory` | `string`, `null` | %INSTALL_PATH.SRC% | *optional* The working directory to execute the process in | +| `options` | `array`, `null` | {"run": null, "log": "verbose"} | Commandline Parameters to pass on to the target or process | +| `silent` | `boolean`, `null` | False | Indicates if the target should run silently, by default this is false. | +| `tty` | `boolean`, `null` | False | Indicates if the target should run in TTY mode | +| `timeout` | `integer`, `null` | 60 | The amount of seconds to wait before the process is killed | + +### JSON Example + +```json +{ + "target": "scripts/foo_bar.bash", + "working_directory": "%INSTALL_PATH.SRC%", + "options": {"run": null, "log": "verbose"}, + "silent": false, + "tty": false, + "timeout": 10 +} +``` + + +------------------------------------------------------------ + +## ExitHandler Object + +An exit handler is executed once the specified exit code is +returned or the process exits with an error or normally, if +an exit handler is specified it will be executed. + +### Object Properties + +| Property Name | Value Type | Example Value | Description | +|---------------|--------------------|---------------|------------------------------------------------------------------------------| +| `message` | `string` | Hello World! | The message to display when the exit handler is triggered | +| `end_process` | `boolean`, `null` | False | *optional* Kills the process after this exit handler is triggered | +| `run` | `string`, `null` | `null` | *optional* A execution policy to execute once this exit handler is triggered | +| `exit_code` | `int`, `null` | 1 | The exit code that triggers this exit handler | +### JSON Example + +```json +{ + "message": "Hello World", + "end_process": false, + "run": null, + "exit_code": 1 +} +``` \ No newline at end of file diff --git a/src/installer/installer b/src/installer/installer index 2121c4b..26732b9 100644 --- a/src/installer/installer +++ b/src/installer/installer @@ -12,12 +12,8 @@ exists($path)) { Console::outError('Missing file \'' . $path . '\', installation failed.', true, 1); exit(1); @@ -202,7 +199,7 @@ // Preform the checksum validation if(!$NCC_BYPASS_CHECKSUM) { - if(!file_exists($NCC_CHECKSUM)) + if(!$NCC_FILESYSTEM->exists($NCC_CHECKSUM)) { Console::outWarning('The file \'checksum.bin\' was not found, the contents of the program cannot be verified to be safe'); } @@ -210,7 +207,16 @@ { Console::out('Running checksum'); - $checksum = ZiProto::decode(file_get_contents(__DIR__ . DIRECTORY_SEPARATOR . 'checksum.bin')); + try + { + $checksum = ZiProto::decode(IO::fread(__DIR__ . DIRECTORY_SEPARATOR . 'checksum.bin')); + } + catch(Exception $e) + { + Console::outError($e->getMessage(), true, 1); + return; + } + $checksum_failed = false; foreach($checksum as $path => $hash) @@ -239,24 +245,33 @@ } } - // Check for required extensions - $curl_available = true; - foreach(Validate::requiredExtensions() as $ext => $installed) + // Check the installed extensions and report + Console::out('Checking installed extensions...'); + $extensions = Validate::requiredExtensions(); + foreach($extensions as $ext => $installed) { - if(!$installed) + if($installed) { - switch($ext) - { - case 'curl': - Console::outWarning('This installer requires the \'curl\' extension to install composer'); - $curl_available = false; - break; - - default: - Console::outWarning('The extension \'' . $ext . '\' is not installed, compatibility without it is not guaranteed'); - break; - } + Console::out("$ext ... " . Console::formatColor("installed", ConsoleColors::LightGreen)); } + else + { + Console::out("$ext ... " . Console::formatColor("missing", ConsoleColors::LightRed)); + } + } + + // Check for curl if the installer requires it + $curl_available = true; + if(!$extensions['curl']) + { + if(getParameter($NCC_ARGS, 'install-composer') !== null) + { + Console::outError('This installer requires the \'curl\' extension to install composer', true, 1); + return; + } + + $curl_available = false; + Console::outWarning('The extension \'curl\' is not installed, the installer will not be able to install composer'); } // Attempt to load version information @@ -283,14 +298,16 @@ try { - Console::out($full_name . ' Version: ' . $component->getVersion()); + Console::out(Console::formatColor($full_name, ConsoleColors::Green) . ' Version: ' . Console::formatColor($component->getVersion(), ConsoleColors::LightMagenta)); } - catch (ComponentVersionNotFoundException $e) + catch (Exception $e) { - Console::outWarning('Cannot determine component version of ' . $full_name); + Console::outWarning('Cannot determine component version of ' . Console::formatColor($full_name, ConsoleColors::Green)); } } + Console::out('Starting installation'); + // Determine the installation path $skip_prompt = false; $install_dir_arg = getParameter($NCC_ARGS, 'install-dir'); @@ -304,7 +321,7 @@ exit(1); } - if(file_exists($install_dir_arg . DIRECTORY_SEPARATOR . 'ncc')) + if($NCC_FILESYSTEM->exists($install_dir_arg . DIRECTORY_SEPARATOR . 'ncc')) { Console::out('NCC Seems to already be installed, the installer will repair/upgrade your current install'); $NCC_INSTALL_PATH = $install_dir_arg; @@ -323,9 +340,9 @@ { $user_input = null; $user_input = Console::getInput("Installation Path (Default: $NCC_INSTALL_PATH): "); - if(strlen($user_input) > 0 && file_exists($user_input) && Validate::unixFilepath($user_input)) + if(strlen($user_input) > 0 && $NCC_FILESYSTEM->exists($user_input) && Validate::unixFilepath($user_input)) { - if(file_exists($user_input . DIRECTORY_SEPARATOR . 'ncc')) + if($NCC_FILESYSTEM->exists($user_input . DIRECTORY_SEPARATOR . 'ncc')) { $NCC_INSTALL_PATH = $user_input; break; @@ -346,7 +363,6 @@ } } - // Determine the data path $skip_prompt = false; $data_dir_arg = getParameter($NCC_ARGS, 'data-dir'); @@ -360,7 +376,7 @@ exit(1); } - if(file_exists($data_dir_arg . DIRECTORY_SEPARATOR . 'package.lck')) + if($NCC_FILESYSTEM->exists($data_dir_arg . DIRECTORY_SEPARATOR . 'package.lck')) { $NCC_DATA_PATH = $data_dir_arg; $skip_prompt = true; @@ -379,9 +395,9 @@ { $user_input = null; $user_input = Console::getInput("Data Path (Default: $NCC_DATA_PATH): "); - if(strlen($user_input) > 0 && file_exists($user_input) && Validate::unixFilepath($user_input)) + if(strlen($user_input) > 0 && $NCC_FILESYSTEM->exists($user_input) && Validate::unixFilepath($user_input)) { - if(file_exists($user_input . DIRECTORY_SEPARATOR . 'package.lck')) + if($NCC_FILESYSTEM->exists($user_input . DIRECTORY_SEPARATOR . 'package.lck')) { $NCC_DATA_PATH = $user_input; break; @@ -409,14 +425,17 @@ { $update_composer = true; } - elseif(getParameter($NCC_ARGS, 'install-composer') !== null) - { - $update_composer = true; - } else { - Console::out("Note: This doesn't affect your current install of composer (if you have composer installed)"); - $update_composer = Console::getBooleanInput('Do you want to install composer for NCC? (Recommended)'); + if(!$NCC_AUTO_MODE) + { + Console::out("Note: This doesn't affect your current install of composer (if you have composer installed)"); + $update_composer = Console::getBooleanInput('Do you want to install composer for NCC? (Recommended)'); + } + else + { + $update_composer = false; + } } } else @@ -424,6 +443,7 @@ $update_composer = false; } + if(!$NCC_AUTO_MODE) { if(!Console::getBooleanInput('Do you want install NCC?')) @@ -435,14 +455,22 @@ // Backup the configuration file $config_backup = null; - if(file_exists($NCC_INSTALL_PATH . DIRECTORY_SEPARATOR . 'ncc.yaml')) + if($NCC_FILESYSTEM->exists($NCC_INSTALL_PATH . DIRECTORY_SEPARATOR . 'ncc.yaml')) { Console::out('ncc.yaml will be updated'); - $config_backup = file_get_contents($NCC_INSTALL_PATH . DIRECTORY_SEPARATOR . 'ncc.yaml'); + try + { + $config_backup = IO::fread($NCC_INSTALL_PATH . DIRECTORY_SEPARATOR . 'ncc.yaml'); + } + catch(Exception $e) + { + Console::outError($e->getMessage(), true, 1); + return; + } } // Prepare installation - if(file_exists($NCC_INSTALL_PATH)) + if($NCC_FILESYSTEM->exists($NCC_INSTALL_PATH)) { try { @@ -455,35 +483,20 @@ } } - // Create the required directories - $required_dirs = [ - $NCC_INSTALL_PATH, - $NCC_DATA_PATH, - $NCC_DATA_PATH . DIRECTORY_SEPARATOR . 'packages', - $NCC_DATA_PATH . DIRECTORY_SEPARATOR . 'cache', - $NCC_DATA_PATH . DIRECTORY_SEPARATOR . 'cache' . DIRECTORY_SEPARATOR . 'repos', - $NCC_DATA_PATH . DIRECTORY_SEPARATOR . 'cache' . DIRECTORY_SEPARATOR . 'downloads', - $NCC_DATA_PATH . DIRECTORY_SEPARATOR . 'config', - $NCC_DATA_PATH . DIRECTORY_SEPARATOR . 'data', - $NCC_DATA_PATH . DIRECTORY_SEPARATOR . 'ext', - ]; + $NCC_FILESYSTEM->mkdir($NCC_INSTALL_PATH, 0755); - $NCC_FILESYSTEM->mkdir($required_dirs, 0755); - $NCC_FILESYSTEM->chmod([$NCC_DATA_PATH . DIRECTORY_SEPARATOR . 'config'], 0777); - $NCC_FILESYSTEM->chmod([$NCC_DATA_PATH . DIRECTORY_SEPARATOR . 'cache'], 0777); - - // Verify directories exist - foreach($required_dirs as $dir) + try { - if(!file_exists($dir)) - { - Console::outError("Cannot create directory '$dir', please verify if you have write permissions to the directory."); - exit(1); - } + Functions::initializeFiles(); + } + catch(Exception $e) + { + Console::outError('Cannot initialize NCC files, ' . $e->getMessage()); + exit(1); } // Install composer - if($curl_available && $update_composer) + if($update_composer) { Console::out('Installing composer for NCC'); @@ -501,11 +514,10 @@ fclose($fp); Console::out('Running composer installer'); - // TODO: Unescaped shell arguments are a security issue $Process = Process::fromShellCommandline(implode(' ', [ $NCC_PHP_EXECUTABLE, - $NCC_INSTALL_PATH . DIRECTORY_SEPARATOR . 'composer-setup.php', - '--install-dir=' . $NCC_INSTALL_PATH, + escapeshellcmd($NCC_INSTALL_PATH . DIRECTORY_SEPARATOR . 'composer-setup.php'), + '--install-dir=' . escapeshellcmd($NCC_INSTALL_PATH), '--filename=composer.phar' ])); $Process->setWorkingDirectory($NCC_INSTALL_PATH); @@ -536,7 +548,15 @@ // Install NCC Console::out('Copying files to \'' . $NCC_INSTALL_PATH . '\''); - $build_files = explode("\n", file_get_contents(__DIR__ . DIRECTORY_SEPARATOR . 'build_files')); + try + { + $build_files = explode("\n", IO::fread(__DIR__ . DIRECTORY_SEPARATOR . 'build_files')); + } + catch(Exception $e) + { + Console::outError($e->getMessage(), true, 1); + return; + } $total_items = count($build_files); $processed_items = 0; @@ -561,7 +581,7 @@ $NCC_FILESYSTEM->copy(__DIR__ . DIRECTORY_SEPARATOR . $file, $NCC_INSTALL_PATH . DIRECTORY_SEPARATOR . $file); $NCC_FILESYSTEM->chmod([$NCC_INSTALL_PATH . DIRECTORY_SEPARATOR . $file], 0755); - if(!file_exists($NCC_INSTALL_PATH . DIRECTORY_SEPARATOR . $file)) + if(!$NCC_FILESYSTEM->exists($NCC_INSTALL_PATH . DIRECTORY_SEPARATOR . $file)) { Console::outError('Cannot create file \'' . $NCC_INSTALL_PATH . DIRECTORY_SEPARATOR . $file . '\', installation failed.'); exit(1); @@ -573,32 +593,19 @@ Console::inlineProgressBar($processed_items, $total_items); } - // Create credential store if needed - Console::out('Processing Credential Store'); - $credential_manager = new CredentialManager(); - - try - { - $credential_manager->constructStore(); - } - catch (AccessDeniedException|\ncc\Exceptions\RuntimeException $e) - { - Console::outError('Cannot construct credential store, ' . $e->getMessage() . ' (Error Code: ' . $e->getCode() . ')'); - } - - try - { - $NCC_FILESYSTEM->touch([PathFinder::getPackageLock(Scopes::System)]); - } - catch (InvalidScopeException $e) - { - Console::outError('Cannot create package lock, ' . $e->getMessage()); - exit(0); - } - // Generate executable shortcut Console::out('Creating shortcut'); - $executable_shortcut = file_get_contents(__DIR__ . DIRECTORY_SEPARATOR . 'ncc.sh'); + + try + { + $executable_shortcut = IO::fread(__DIR__ . DIRECTORY_SEPARATOR . 'ncc.sh'); + } + catch(Exception $e) + { + Console::outError($e->getMessage(), true, 1); + return; + } + $executable_shortcut = str_ireplace('%php_exec', $NCC_PHP_EXECUTABLE, $executable_shortcut); $executable_shortcut = str_ireplace('%ncc_exec', $NCC_INSTALL_PATH . DIRECTORY_SEPARATOR . 'ncc', $executable_shortcut); @@ -611,41 +618,65 @@ foreach($bin_paths as $path) { // Delete old versions of the executable shortcuts. - if(file_exists($path . DIRECTORY_SEPARATOR . 'ncc')) + if($NCC_FILESYSTEM->exists($path . DIRECTORY_SEPARATOR . 'ncc')) { $NCC_FILESYSTEM->remove($path . DIRECTORY_SEPARATOR . 'ncc'); } - if($NCC_FILESYSTEM->exists([$path])) + if($NCC_FILESYSTEM->exists($path)) { - file_put_contents($path . DIRECTORY_SEPARATOR . 'ncc', $executable_shortcut); - $NCC_FILESYSTEM->chmod([$path . DIRECTORY_SEPARATOR . 'ncc'], 0755); + try + { + IO::fwrite($path . DIRECTORY_SEPARATOR . 'ncc', $executable_shortcut); + break; + } + catch (Exception $e) + { + Console::outException($e->getMessage(), $e, 1); + return; + } } } // Register the ncc extension Console::out('Registering extension'); - $extension_shortcut = file_get_contents(__DIR__ . DIRECTORY_SEPARATOR . 'extension'); + try + { + $extension_shortcut = IO::fread(__DIR__ . DIRECTORY_SEPARATOR . 'extension'); + } + catch(Exception $e) + { + Console::outError($e->getMessage(), true, 1); + return; + } $extension_shortcut = str_ireplace('%ncc_install', $NCC_INSTALL_PATH, $extension_shortcut); // Remove all the old extensions first. /** * @param string $path - * @param Filesystem $NCC_FILESYSTEM + * @param Filesystem $filesystem * @param string $extension_shortcut * @return bool */ - function install_extension(string $path, Filesystem $NCC_FILESYSTEM, string $extension_shortcut): bool + function install_extension(string $path, Filesystem $filesystem, string $extension_shortcut): bool { - if (file_exists($path . DIRECTORY_SEPARATOR . 'ncc')) + if ($filesystem->exists($path . DIRECTORY_SEPARATOR . 'ncc')) { - $NCC_FILESYSTEM->remove($path . DIRECTORY_SEPARATOR . 'ncc'); + $filesystem->remove($path . DIRECTORY_SEPARATOR . 'ncc'); } - file_put_contents($path . DIRECTORY_SEPARATOR . 'ncc', $extension_shortcut); - $NCC_FILESYSTEM->chmod([$path . DIRECTORY_SEPARATOR . 'ncc'], 0755); + try + { + IO::fwrite($path . DIRECTORY_SEPARATOR . 'ncc', $extension_shortcut); + } + catch (\ncc\Exceptions\IOException $e) + { + Console::outException($e->getMessage(), $e, 1); + return false; + } - if (file_exists($path . DIRECTORY_SEPARATOR . 'ncc')) { + if ($filesystem->exists($path . DIRECTORY_SEPARATOR . 'ncc')) + { return true; } @@ -745,7 +776,15 @@ Console::out('Updating ncc.yaml'); } - file_put_contents($NCC_INSTALL_PATH . DIRECTORY_SEPARATOR . 'ncc.yaml', Yaml::dump($config_obj)); + try + { + IO::fwrite($NCC_INSTALL_PATH . DIRECTORY_SEPARATOR . 'ncc.yaml', Yaml::dump($config_obj)); + } + catch (\ncc\Exceptions\IOException $e) + { + Console::outException($e->getMessage(), $e, 1); + return; + } Console::out('NCC version: ' . NCC_VERSION_NUMBER . ' has been successfully installed'); Console::out('For licensing information see \'' . $NCC_INSTALL_PATH . DIRECTORY_SEPARATOR . 'LICENSE\' or run \'ncc help --license\''); diff --git a/src/ncc/Abstracts/ConstantReferences.php b/src/ncc/Abstracts/ConstantReferences.php new file mode 100644 index 0000000..43cf7ae --- /dev/null +++ b/src/ncc/Abstracts/ConstantReferences.php @@ -0,0 +1,14 @@ +""|]*[%])|([a-zA-Z][:])|(\\\\))((\\\\{1})|((\\\\{1})[^\\\\]([^\/:*?<>""|]*))+)$/m'; const ConstantName = '/^([^\x00-\x7F]|[\w_\ \.\+\-]){2,64}$/'; + + const ExecutionPolicyName = '/^[_$a-zA-Z\xA0-\uFFFF][_$a-zA-Z0-9\xA0-\uFFFF]*$/m'; } \ No newline at end of file diff --git a/src/ncc/Abstracts/Runners.php b/src/ncc/Abstracts/Runners.php new file mode 100644 index 0000000..5a0e052 --- /dev/null +++ b/src/ncc/Abstracts/Runners.php @@ -0,0 +1,8 @@ +Project->Compiler->Extension)) - { - case CompilerExtensions::PHP: - /** @var CompilerInterface $Compiler */ - $Compiler = new Compiler($ProjectConfiguration); - break; - - default: - Console::outError('The extension '. $ProjectConfiguration->Project->Compiler->Extension . ' is not supported', true, 1); - return; - } - - $build_configuration = BuildConfigurationValues::DefaultConfiguration; - - if(isset($args['config'])) - { - $build_configuration = $args['config']; - } - - // Auto-resolve to the default configuration if `default` is used or not specified - if($build_configuration == BuildConfigurationValues::DefaultConfiguration) - { - $build_configuration = $ProjectConfiguration->Build->DefaultConfiguration; - } - - try - { - $ProjectConfiguration->Build->getBuildConfiguration($build_configuration); - } - catch (BuildConfigurationNotFoundException $e) - { - Console::outException('The build configuration \'' . $build_configuration . '\' does not exist in the project configuration', $e, 1); - return; - } - - Console::out( - ' ===== BUILD INFO ===== ' . PHP_EOL . - ' Package Name: ' . $ProjectConfiguration->Assembly->Package . PHP_EOL . - ' Version: ' . $ProjectConfiguration->Assembly->Version . PHP_EOL . - ' Compiler Extension: ' . $ProjectConfiguration->Project->Compiler->Extension . PHP_EOL . - ' Compiler Version: ' . $ProjectConfiguration->Project->Compiler->MaximumVersion . ' - ' . $ProjectConfiguration->Project->Compiler->MinimumVersion . PHP_EOL . - ' Build Configuration: ' . $build_configuration . PHP_EOL - ); - - Console::out('Preparing package'); - - try - { - $Compiler->prepare($project_path, $build_configuration); + $ProjectManager = new ProjectManager($project_path); + $ProjectManager->load(); } catch (Exception $e) { - Console::outException('The package preparation process failed', $e, 1); + Console::outException('Failed to load Project Configuration (project.json)', $e, 1); return; } - Console::out('Compiling package'); - + // Build the project try { - $Compiler->build($project_path); + $build_configuration = BuildConfigurationValues::DefaultConfiguration; + if(isset($args['config'])) + { + $build_configuration = $args['config']; + } + + $output = $ProjectManager->build($build_configuration); + + Console::out('Successfully built ' . $output); + exit(0); } catch (Exception $e) { - Console::outException('Build Failed', $e, 1); + Console::outException('Failed to build project', $e, 1); return; } - exit(0); } /** diff --git a/src/ncc/CLI/Functions.php b/src/ncc/CLI/Functions.php deleted file mode 100644 index dceff6e..0000000 --- a/src/ncc/CLI/Functions.php +++ /dev/null @@ -1,31 +0,0 @@ - 0) - { - $out = str_pad($out, $padding, $pad_string, $pad_type); - } - - if($eol) - { - $out = $out . PHP_EOL; - } - - print($out); - } - } \ No newline at end of file diff --git a/src/ncc/CLI/Main.php b/src/ncc/CLI/Main.php index c8fe242..a2a269f 100644 --- a/src/ncc/CLI/Main.php +++ b/src/ncc/CLI/Main.php @@ -1,8 +1,11 @@ getMessage() . ' (Code: ' . $e->getCode() . ')', $e, 1); + Console::outException($e->getMessage() . ' (Code: ' . $e->getCode() . ')', $e, 1); exit(1); } } } + /** + * @return mixed + */ + public static function getArgs() + { + return self::$args; + } + + /** + * @return string + */ + public static function getLogLevel(): string + { + if(self::$log_level == null) + self::$log_level = LogLevel::Info; + return self::$log_level; + } + } \ No newline at end of file diff --git a/src/ncc/CLI/PackageManagerMenu.php b/src/ncc/CLI/PackageManagerMenu.php new file mode 100644 index 0000000..319eac9 --- /dev/null +++ b/src/ncc/CLI/PackageManagerMenu.php @@ -0,0 +1,345 @@ +getInstalledPackages(); + } + catch (Exception $e) + { + unset($e); + Console::out('No packages installed'); + exit(0); + } + + foreach($installed_packages as $package => $versions) + { + if(count($versions) == 0) + { + continue; + } + + foreach($versions as $version) + { + try + { + $package_version = $package_manager->getPackageVersion($package, $version); + if($package_version == null) + throw new Exception(); + + Console::out(sprintf('%s==%s (%s)', + Console::formatColor($package, ConsoleColors::LightGreen), + Console::formatColor($version, ConsoleColors::LightMagenta), + $package_manager->getPackageVersion($package, $version)->Compiler->Extension + )); + } + catch(Exception $e) + { + unset($e); + Console::out(sprintf('%s==%s', + Console::formatColor($package, ConsoleColors::LightGreen), + Console::formatColor($version, ConsoleColors::LightMagenta) + )); + } + } + } + } + + /** + * @param $args + * @return void + * @throws AccessDeniedException + * @throws FileNotFoundException + */ + private static function installPackage($args): void + { + $path = ($args['path'] ?? $args['p']); + $package_manager = new PackageManager(); + + if(Resolver::resolveScope() !== Scopes::System) + throw new AccessDeniedException('Insufficient permission to install packages'); + + if(!file_exists($path) || !is_file($path) || !is_readable($path)) + throw new FileNotFoundException('The specified file \'' . $path .' \' does not exist or is not readable.'); + + $user_confirmation = false; + if(isset($args['y']) || isset($args['Y'])) + { + $user_confirmation = (bool)($args['y'] ?? $args['Y']); + } + + try + { + $package = Package::load($path); + } + catch(Exception $e) + { + Console::outException('Error while loading package', $e, 1); + return; + } + + Console::out('Package installation details' . PHP_EOL); + if(!is_null($package->Assembly->UUID)) + Console::out(' UUID: ' . Console::formatColor($package->Assembly->UUID, ConsoleColors::LightGreen)); + if(!is_null($package->Assembly->Package)) + Console::out(' Package: ' . Console::formatColor($package->Assembly->Package, ConsoleColors::LightGreen)); + if(!is_null($package->Assembly->Name)) + Console::out(' Name: ' . Console::formatColor($package->Assembly->Name, ConsoleColors::LightGreen)); + if(!is_null($package->Assembly->Version)) + Console::out(' Version: ' . Console::formatColor($package->Assembly->Version, ConsoleColors::LightGreen)); + if(!is_null($package->Assembly->Description)) + Console::out(' Description: ' . Console::formatColor($package->Assembly->Description, ConsoleColors::LightGreen)); + if(!is_null($package->Assembly->Product)) + Console::out(' Product: ' . Console::formatColor($package->Assembly->Product, ConsoleColors::LightGreen)); + if(!is_null($package->Assembly->Company)) + Console::out(' Company: ' . Console::formatColor($package->Assembly->Company, ConsoleColors::LightGreen)); + if(!is_null($package->Assembly->Copyright)) + Console::out(' Copyright: ' . Console::formatColor($package->Assembly->Copyright, ConsoleColors::LightGreen)); + if(!is_null($package->Assembly->Trademark)) + Console::out(' Trademark: ' . Console::formatColor($package->Assembly->Trademark, ConsoleColors::LightGreen)); + Console::out((string)null); + + if(count($package->Dependencies) > 0) + { + $dependencies = []; + foreach($package->Dependencies as $dependency) + { + $dependencies[] = sprintf('%s v%s', + Console::formatColor($dependency->Name, ConsoleColors::Green), + Console::formatColor($dependency->Version, ConsoleColors::LightMagenta) + ); + } + + Console::out('The following dependencies will be installed:'); + Console::out(sprintf(' %s', implode(', ', $dependencies)) . PHP_EOL); + } + + Console::out(sprintf('Extension: %s', + Console::formatColor($package->Header->CompilerExtension->Extension, ConsoleColors::Green) + )); + + if($package->Header->CompilerExtension->MaximumVersion !== null) + Console::out(sprintf('Maximum Version: %s', + Console::formatColor($package->Header->CompilerExtension->MaximumVersion, ConsoleColors::LightMagenta) + )); + + if($package->Header->CompilerExtension->MinimumVersion !== null) + Console::out(sprintf('Minimum Version: %s', + Console::formatColor($package->Header->CompilerExtension->MinimumVersion, ConsoleColors::LightMagenta) + )); + + if(!$user_confirmation) + $user_confirmation = Console::getBooleanInput(sprintf('Do you want to install %s', $package->Assembly->Package)); + + if($user_confirmation) + { + try + { + $package_manager->install($path); + } + catch(Exception $e) + { + Console::outException('Installation Failed', $e, 1); + } + + return; + } + + Console::outError('User cancelled installation', true, 1); + } + + /** + * Uninstalls a version of a package or all versions of a package + * + * @param $args + * @return void + * @throws VersionNotFoundException + */ + private static function uninstallPackage($args): void + { + $selected_package = ($args['package'] ?? $args['pkg']); + $selected_version = null; + if(isset($args['v'])) + $selected_version = $args['v']; + if(isset($args['version'])) + $selected_version = $args['version']; + + $user_confirmation = null; + // For undefined array key warnings + if(isset($args['y']) || isset($args['Y'])) + $user_confirmation = (bool)($args['y'] ?? $args['Y']); + + if($selected_package == null) + Console::outError('Missing argument \'package\'', true, 1); + + $package_manager = new PackageManager(); + + try + { + $package_entry = $package_manager->getPackage($selected_package); + } + catch (PackageLockException $e) + { + Console::outException('PackageLock error', $e, 1); + return; + } + + $version_entry = null; + if($version_entry !== null && $package_entry !== null) + /** @noinspection PhpUnhandledExceptionInspection */ + /** @noinspection PhpRedundantOptionalArgumentInspection */ + $version_entry = $package_entry->getVersion($version_entry, false); + + if($package_entry == null) + { + Console::outError(sprintf('Package "%s" is not installed', $selected_package), true, 1); + return; + } + + if($version_entry == null & $selected_version !== null) + { + Console::outError(sprintf('Package "%s==%s" is not installed', $selected_package, $selected_version), true, 1); + return; + } + + if($user_confirmation == null) + { + if($selected_version !== null) + { + if(!Console::getBooleanInput(sprintf('Do you want to uninstall %s==%s', $selected_package, $selected_version))) + { + Console::outError('User cancelled operation', true, 1); + return; + } + } + else + { + if(!Console::getBooleanInput(sprintf('Do you want to uninstall all versions of %s', $selected_package))) + { + Console::outError('User cancelled operation', true, 1); + return; + } + } + } + + try + { + if($selected_version !== null) + { + $package_manager->uninstallPackageVersion($selected_package, $selected_version); + } + else + { + $package_manager->uninstallPackage($selected_package); + } + } + catch(Exception $e) + { + Console::outException('Uninstallation failed', $e, 1); + return; + } + } + + /** + * Displays the main options section + * + * @return void + */ + private static function displayOptions(): void + { + $options = [ + new CliHelpSection(['help'], 'Displays this help menu about the value command'), + new CliHelpSection(['install', '--path', '-p'], 'Installs a specified NCC package file'), + new CliHelpSection(['list'], 'Lists all installed packages on the system'), + ]; + + $options_padding = Functions::detectParametersPadding($options) + 4; + + Console::out('Usage: ncc install {command} [options]'); + Console::out('Options:' . PHP_EOL); + foreach($options as $option) + { + Console::out(' ' . $option->toString($options_padding)); + } + } + } \ No newline at end of file diff --git a/src/ncc/Classes/NccExtension/ConstantCompiler.php b/src/ncc/Classes/NccExtension/ConstantCompiler.php new file mode 100644 index 0000000..5ab4597 --- /dev/null +++ b/src/ncc/Classes/NccExtension/ConstantCompiler.php @@ -0,0 +1,129 @@ +Name, $input); + $input = str_replace(AssemblyConstants::AssemblyPackage, $assembly->Package, $input); + $input = str_replace(AssemblyConstants::AssemblyDescription, $assembly->Description, $input); + $input = str_replace(AssemblyConstants::AssemblyCompany, $assembly->Company, $input); + $input = str_replace(AssemblyConstants::AssemblyProduct, $assembly->Product, $input); + $input = str_replace(AssemblyConstants::AssemblyCopyright, $assembly->Copyright, $input); + $input = str_replace(AssemblyConstants::AssemblyTrademark, $assembly->Trademark, $input); + $input = str_replace(AssemblyConstants::AssemblyVersion, $assembly->Version, $input); + $input = str_replace(AssemblyConstants::AssemblyUid, $assembly->UUID, $input); + + return $input; + } + + /** + * Compiles build constants about the NCC build (Usually used during compiling time) + * + * @param string|null $input + * @return string|null + * @noinspection PhpUnnecessaryLocalVariableInspection + */ + public static function compileBuildConstants(?string $input): ?string + { + if($input == null) + return null; + + $input = str_replace(BuildConstants::CompileTimestamp, time(), $input); + $input = str_replace(BuildConstants::NccBuildVersion, NCC_VERSION_NUMBER, $input); + $input = str_replace(BuildConstants::NccBuildFlags, implode(' ', NCC_VERSION_FLAGS), $input); + $input = str_replace(BuildConstants::NccBuildBranch, NCC_VERSION_BRANCH, $input); + + return $input; + } + + /** + * Compiles installation constants (Usually used during compiling time) + * + * @param string|null $input + * @param InstallationPaths $installationPaths + * @return string|null + * @noinspection PhpUnnecessaryLocalVariableInspection + */ + public static function compileInstallConstants(?string $input, InstallationPaths $installationPaths): ?string + { + if($input == null) + return null; + + $input = str_replace(InstallConstants::InstallationPath, $installationPaths->getInstallationPath(), $input); + $input = str_replace(InstallConstants::BinPath, $installationPaths->getBinPath(), $input); + $input = str_replace(InstallConstants::SourcePath, $installationPaths->getSourcePath(), $input); + $input = str_replace(InstallConstants::DataPath, $installationPaths->getDataPath(), $input); + + return $input; + } + + /** + * Compiles DateTime constants from a Unix Timestamp + * + * @param string|null $input + * @param int $timestamp + * @return string|null + * @noinspection PhpUnnecessaryLocalVariableInspection + */ + public static function compileDateTimeConstants(?string $input, int $timestamp): ?string + { + if($input == null) + return null; + + $input = str_replace(DateTimeConstants::d, date('d', $timestamp), $input); + $input = str_replace(DateTimeConstants::D, date('D', $timestamp), $input); + $input = str_replace(DateTimeConstants::j, date('j', $timestamp), $input); + $input = str_replace(DateTimeConstants::l, date('l', $timestamp), $input); + $input = str_replace(DateTimeConstants::N, date('N', $timestamp), $input); + $input = str_replace(DateTimeConstants::S, date('S', $timestamp), $input); + $input = str_replace(DateTimeConstants::w, date('w', $timestamp), $input); + $input = str_replace(DateTimeConstants::z, date('z', $timestamp), $input); + $input = str_replace(DateTimeConstants::W, date('W', $timestamp), $input); + $input = str_replace(DateTimeConstants::F, date('F', $timestamp), $input); + $input = str_replace(DateTimeConstants::m, date('m', $timestamp), $input); + $input = str_replace(DateTimeConstants::M, date('M', $timestamp), $input); + $input = str_replace(DateTimeConstants::n, date('n', $timestamp), $input); + $input = str_replace(DateTimeConstants::t, date('t', $timestamp), $input); + $input = str_replace(DateTimeConstants::L, date('L', $timestamp), $input); + $input = str_replace(DateTimeConstants::o, date('o', $timestamp), $input); + $input = str_replace(DateTimeConstants::Y, date('Y', $timestamp), $input); + $input = str_replace(DateTimeConstants::y, date('y', $timestamp), $input); + $input = str_replace(DateTimeConstants::a, date('a', $timestamp), $input); + $input = str_replace(DateTimeConstants::A, date('A', $timestamp), $input); + $input = str_replace(DateTimeConstants::B, date('B', $timestamp), $input); + $input = str_replace(DateTimeConstants::g, date('g', $timestamp), $input); + $input = str_replace(DateTimeConstants::G, date('G', $timestamp), $input); + $input = str_replace(DateTimeConstants::h, date('h', $timestamp), $input); + $input = str_replace(DateTimeConstants::H, date('H', $timestamp), $input); + $input = str_replace(DateTimeConstants::i, date('i', $timestamp), $input); + $input = str_replace(DateTimeConstants::s, date('s', $timestamp), $input); + $input = str_replace(DateTimeConstants::c, date('c', $timestamp), $input); + $input = str_replace(DateTimeConstants::r, date('r', $timestamp), $input); + $input = str_replace(DateTimeConstants::u, date('u', $timestamp), $input); + + return $input; + } + } \ No newline at end of file diff --git a/src/ncc/Classes/NccExtension/PackageCompiler.php b/src/ncc/Classes/NccExtension/PackageCompiler.php new file mode 100644 index 0000000..8ac9bae --- /dev/null +++ b/src/ncc/Classes/NccExtension/PackageCompiler.php @@ -0,0 +1,307 @@ +getProjectConfiguration(); + + if(Main::getLogLevel() !== null && Resolver::checkLogLevel(LogLevel::Debug, Main::getLogLevel())) + { + foreach($configuration->Assembly->toArray() as $prop => $value) + Console::outDebug(sprintf('assembly.%s: %s', $prop, ($value ?? 'n/a'))); + foreach($configuration->Project->Compiler->toArray() as $prop => $value) + Console::outDebug(sprintf('compiler.%s: %s', $prop, ($value ?? 'n/a'))); + } + + // Select the correct compiler for the specified extension + /** @noinspection PhpSwitchCanBeReplacedWithMatchExpressionInspection */ + switch(strtolower($configuration->Project->Compiler->Extension)) + { + case CompilerExtensions::PHP: + /** @var CompilerInterface $Compiler */ + $Compiler = new Compiler($configuration, $manager->getProjectPath()); + break; + + default: + throw new UnsupportedCompilerExtensionException('The compiler extension \'' . $configuration->Project->Compiler->Extension . '\' is not supported'); + } + + $build_configuration = $configuration->Build->getBuildConfiguration($build_configuration)->Name; + $Compiler->prepare($build_configuration); + $Compiler->build(); + + return PackageCompiler::writePackage( + $manager->getProjectPath(), $Compiler->getPackage(), $configuration, $build_configuration + ); + } + + /** + * Compiles the execution policies of the package + * + * @param string $path + * @param ProjectConfiguration $configuration + * @return array + * @throws AccessDeniedException + * @throws FileNotFoundException + * @throws IOException + * @throws UnsupportedRunnerException + */ + public static function compileExecutionPolicies(string $path, ProjectConfiguration $configuration): array + { + if(count($configuration->ExecutionPolicies) == 0) + return []; + + Console::out('Compiling Execution Policies'); + $total_items = count($configuration->ExecutionPolicies); + $execution_units = []; + $processed_items = 0; + + /** @var ProjectConfiguration\ExecutionPolicy $policy */ + foreach($configuration->ExecutionPolicies as $policy) + { + if($total_items > 5) + { + Console::inlineProgressBar($processed_items, $total_items); + } + + $unit_path = Functions::correctDirectorySeparator($path . $policy->Execute->Target); + $execution_units[] = Functions::compileRunner($unit_path, $policy); + } + + if(ncc::cliMode() && $total_items > 5) + print(PHP_EOL); + + return $execution_units; + } + + /** + * Writes the finished package to disk, returns the output path + * + * @param string $path + * @param Package $package + * @param ProjectConfiguration $configuration + * @param string $build_configuration + * @return string + * @throws BuildConfigurationNotFoundException + * @throws BuildException + * @throws IOException + */ + public static function writePackage(string $path, Package $package, ProjectConfiguration $configuration, string $build_configuration=BuildConfigurationValues::DefaultConfiguration): string + { + // Write the package to disk + $FileSystem = new Filesystem(); + $BuildConfiguration = $configuration->Build->getBuildConfiguration($build_configuration); + if($FileSystem->exists($path . $BuildConfiguration->OutputPath)) + { + try + { + $FileSystem->remove($path . $BuildConfiguration->OutputPath); + } + catch(\ncc\ThirdParty\Symfony\Filesystem\Exception\IOException $e) + { + throw new BuildException('Cannot delete directory \'' . $path . $BuildConfiguration->OutputPath . '\', ' . $e->getMessage(), $e); + } + } + + // Finally write the package to the disk + $FileSystem->mkdir($path . $BuildConfiguration->OutputPath); + $output_file = $path . $BuildConfiguration->OutputPath . DIRECTORY_SEPARATOR . $package->Assembly->Package . '.ncc'; + $FileSystem->touch($output_file); + + try + { + $package->save($output_file); + } + catch(Exception $e) + { + throw new IOException('Cannot write to output file', $e); + } + + return $output_file; + } + + /** + * Compiles the special formatted constants + * + * @param Package $package + * @param int $timestamp + * @return array + */ + public static function compileRuntimeConstants(Package $package, int $timestamp): array + { + $compiled_constants = []; + + foreach($package->Header->RuntimeConstants as $name => $value) + { + $compiled_constants[$name] = self::compileConstants($value, [ + ConstantReferences::Assembly => $package->Assembly, + ConstantReferences::DateTime => $timestamp, + ConstantReferences::Build => null + ]); + } + + return $compiled_constants; + } + + /** + * Compiles the constants in the package object + * + * @param Package $package + * @param array $refs + * @return void + */ + public static function compilePackageConstants(Package &$package, array $refs): void + { + if($package->Assembly !== null) + { + $assembly = []; + foreach($package->Assembly->toArray() as $key => $value) + { + $assembly[$key] = self::compileConstants($value, $refs); + } + $package->Assembly = Assembly::fromArray($assembly); + unset($assembly); + } + + if($package->ExecutionUnits !== null && count($package->ExecutionUnits) > 0) + { + $units = []; + foreach($package->ExecutionUnits as $executionUnit) + { + $units[] = self::compileExecutionUnitConstants($executionUnit, $refs); + } + $package->ExecutionUnits = $units; + unset($units); + } + } + + /** + * Compiles the constants in a given execution unit + * + * @param Package\ExecutionUnit $unit + * @param array $refs + * @return Package\ExecutionUnit + */ + public static function compileExecutionUnitConstants(Package\ExecutionUnit $unit, array $refs): Package\ExecutionUnit + { + $unit->ExecutionPolicy->Message = self::compileConstants($unit->ExecutionPolicy->Message, $refs); + + if($unit->ExecutionPolicy->ExitHandlers !== null) + { + if($unit->ExecutionPolicy->ExitHandlers->Success !== null) + { + $unit->ExecutionPolicy->ExitHandlers->Success->Message = self::compileConstants($unit->ExecutionPolicy->ExitHandlers->Success->Message, $refs); + } + + if($unit->ExecutionPolicy->ExitHandlers->Error !== null) + { + $unit->ExecutionPolicy->ExitHandlers->Error->Message = self::compileConstants($unit->ExecutionPolicy->ExitHandlers->Error->Message, $refs); + } + + if($unit->ExecutionPolicy->ExitHandlers->Warning !== null) + { + $unit->ExecutionPolicy->ExitHandlers->Warning->Error = self::compileConstants($unit->ExecutionPolicy->ExitHandlers->Warning->Message, $refs); + } + } + + if($unit->ExecutionPolicy->Execute !== null) + { + if($unit->ExecutionPolicy->Execute->Target !== null) + { + $unit->ExecutionPolicy->Execute->Target = self::compileConstants($unit->ExecutionPolicy->Execute->Target, $refs); + } + + if($unit->ExecutionPolicy->Execute->WorkingDirectory !== null) + { + $unit->ExecutionPolicy->Execute->WorkingDirectory = self::compileConstants($unit->ExecutionPolicy->Execute->WorkingDirectory, $refs); + } + + if($unit->ExecutionPolicy->Execute->Options !== null && count($unit->ExecutionPolicy->Execute->Options) > 0) + { + $options = []; + foreach($unit->ExecutionPolicy->Execute->Options as $key=>$value) + { + $options[self::compileConstants($key, $refs)] = self::compileConstants($value, $refs); + } + $unit->ExecutionPolicy->Execute->Options = $options; + } + } + + return $unit; + } + + /** + * Compiles multiple types of constants + * + * @param string|null $value + * @param array $refs + * @return string|null + */ + public static function compileConstants(?string $value, array $refs): ?string + { + if($value == null) + return null; + + if(isset($refs[ConstantReferences::Assembly])) + $value = ConstantCompiler::compileAssemblyConstants($value, $refs[ConstantReferences::Assembly]); + + if(isset($refs[ConstantReferences::Build])) + $value = ConstantCompiler::compileBuildConstants($value); + + if(isset($refs[ConstantReferences::DateTime])) + $value = ConstantCompiler::compileDateTimeConstants($value, $refs[ConstantReferences::DateTime]); + + if(isset($refs[ConstantReferences::Install])) + $value = ConstantCompiler::compileInstallConstants($value, $refs[ConstantReferences::Install]); + + return $value; + } + } \ No newline at end of file diff --git a/src/ncc/Classes/NccExtension/Runner.php b/src/ncc/Classes/NccExtension/Runner.php new file mode 100644 index 0000000..2133137 --- /dev/null +++ b/src/ncc/Classes/NccExtension/Runner.php @@ -0,0 +1,47 @@ +addUnit($package, $version, $unit, true); + $ExecutionPointerManager->executeUnit($package, $version, $unit->ExecutionPolicy->Name); + $ExecutionPointerManager->cleanTemporaryUnits();; + } + } \ No newline at end of file diff --git a/src/ncc/Classes/PackageParser.php b/src/ncc/Classes/PackageParser.php deleted file mode 100644 index 1d54da1..0000000 --- a/src/ncc/Classes/PackageParser.php +++ /dev/null @@ -1,42 +0,0 @@ -PackagePath = $path; - $this->parseFile(); - } - - private function parseFile() - { - if(file_exists($this->PackagePath) == false) - { - throw new FileNotFoundException('The given package path \'' . $this->PackagePath . '\' does not exist'); - } - - if(is_file($this->PackagePath) == false) - { - throw new FileNotFoundException('The given package path \'' . $this->PackagePath . '\' is not a file'); - } - - $file_handler = fopen($this->PackagePath, 'rb'); - $header = fread($file_handler, 14); - } - } \ No newline at end of file diff --git a/src/ncc/Classes/PhpExtension/AutoloaderGenerator.php b/src/ncc/Classes/PhpExtension/AutoloaderGenerator.php deleted file mode 100644 index 2f94c89..0000000 --- a/src/ncc/Classes/PhpExtension/AutoloaderGenerator.php +++ /dev/null @@ -1,120 +0,0 @@ -project = $project; - } - - /** - * Processes the project and generates the autoloader source code. - * - * @param string $src - * @param string $output - * @param bool $static - * @return string - * @throws AutoloadGeneratorException - * @throws CollectorException - * @throws Exception - * @throws NoUnitsFoundException - */ - public function generateAutoload(string $src, string $output, bool $static=false): string - { - // Construct configuration - $configuration = new Config([$src]); - $configuration->setFollowSymlinks(false); // Don't follow symlinks, it won't work on some systems. - $configuration->setOutputFile($output); - $configuration->setTrusting(false); // Paranoid - // Official PHP file extensions that are missing from the default configuration (whatever) - $configuration->setInclude(ComponentFileExtensions::Php); - - // Construct factory - $factory = new Factory(); - $factory->setConfig($configuration); - - // Create Collector - $result = self::runCollector($factory, $configuration); - - // Exception raises when there are no files in the project that can be processed by the autoloader - if(!$result->hasUnits()) - { - throw new NoUnitsFoundException('No units were found in the project'); - } - - if(!$result->hasDuplicates()) - { - foreach($result->getDuplicates() as $unit => $files) - { - Console::outWarning((count($files) -1). ' duplicate unit(s) detected in the project: ' . $unit); - } - } - - $template = @file_get_contents($configuration->getTemplate()); - if ($template === false) - { - throw new AutoloadGeneratorException("Failed to read the template file '" . $configuration->getTemplate() . "'"); - } - - $builder = $factory->getRenderer($result); - return $builder->render($template); - } - - /** - * Iterates through the target directories through the collector and returns the collector results. - * - * @param Factory $factory - * @param Config $config - * @return CollectorResult - * @throws CollectorException - * @throws Exception - */ - private static function runCollector(Factory $factory, Config $config): CollectorResult - { - $collector = $factory->getCollector(); - foreach($config->getDirectories() as $directory) - { - if(is_dir($directory)) - { - $scanner = $factory->getScanner()->getIterator($directory); - $collector->processDirectory($scanner); - unset($scanner); - } - else - { - $file = new SplFileInfo($directory); - $filter = $factory->getFilter(new ArrayIterator(array($file))); - foreach($filter as $file) - { - $collector->processFile($file); - } - } - } - - return $collector->getResult(); - } - - } \ No newline at end of file diff --git a/src/ncc/Classes/PhpExtension/Compiler.php b/src/ncc/Classes/PhpExtension/Compiler.php index 4257002..d6df577 100644 --- a/src/ncc/Classes/PhpExtension/Compiler.php +++ b/src/ncc/Classes/PhpExtension/Compiler.php @@ -8,22 +8,26 @@ use FilesystemIterator; use ncc\Abstracts\ComponentFileExtensions; use ncc\Abstracts\ComponentDataType; + use ncc\Abstracts\ConstantReferences; use ncc\Abstracts\Options\BuildConfigurationValues; + use ncc\Classes\NccExtension\PackageCompiler; + use ncc\Exceptions\AccessDeniedException; use ncc\Exceptions\BuildConfigurationNotFoundException; use ncc\Exceptions\BuildException; + use ncc\Exceptions\FileNotFoundException; + use ncc\Exceptions\IOException; use ncc\Exceptions\PackagePreparationFailedException; + use ncc\Exceptions\UnsupportedRunnerException; use ncc\Interfaces\CompilerInterface; use ncc\ncc; use ncc\Objects\Package; use ncc\Objects\ProjectConfiguration; - use ncc\ThirdParty\nikic\PhpParser\Error; use ncc\ThirdParty\nikic\PhpParser\ParserFactory; - use ncc\ThirdParty\Symfony\Filesystem\Exception\IOException; - use ncc\ThirdParty\Symfony\Filesystem\Filesystem; use ncc\ThirdParty\theseer\DirectoryScanner\DirectoryScanner; use ncc\Utilities\Base64; use ncc\Utilities\Console; use ncc\Utilities\Functions; + use ncc\Utilities\IO; use SplFileInfo; class Compiler implements CompilerInterface @@ -39,28 +43,30 @@ private $package; /** - * @var ProjectConfiguration\BuildConfiguration|null + * @var string */ - private $selected_build_configuration; + private $path; /** * @param ProjectConfiguration $project + * @param string $path */ - public function __construct(ProjectConfiguration $project) + public function __construct(ProjectConfiguration $project, string $path) { $this->project = $project; + $this->path = $path; } /** * Prepares the PHP package by generating the Autoloader and detecting all components & resources * This function must be called before calling the build function, otherwise the operation will fail * - * @param string $path * @param string $build_configuration * @return void * @throws PackagePreparationFailedException + * @throws BuildConfigurationNotFoundException */ - public function prepare(string $path, string $build_configuration=BuildConfigurationValues::DefaultConfiguration): void + public function prepare(string $build_configuration=BuildConfigurationValues::DefaultConfiguration): void { try { @@ -72,40 +78,29 @@ throw new PackagePreparationFailedException($e->getMessage(), $e); } - // Auto-select the default build configuration - if($build_configuration == BuildConfigurationValues::DefaultConfiguration) - { - $build_configuration = $this->project->Build->DefaultConfiguration; - } - // Select the build configuration - try - { - $this->selected_build_configuration = $this->project->Build->getBuildConfiguration($build_configuration); - } - catch (BuildConfigurationNotFoundException $e) - { - throw new PackagePreparationFailedException($e->getMessage(), $e); - } + $selected_build_configuration = $this->project->Build->getBuildConfiguration($build_configuration); // Create the package object $this->package = new Package(); $this->package->Assembly = $this->project->Assembly; $this->package->Dependencies = $this->project->Build->Dependencies; + $this->package->MainExecutionPolicy = $this->project->Build->Main; // Add both the defined constants from the build configuration and the global constants. // Global constants are overridden - $this->package->Header->RuntimeConstants = array_merge($this->selected_build_configuration->DefineConstants, $this->package->Header->RuntimeConstants); - $this->package->Header->RuntimeConstants = array_merge($this->project->Build->DefineConstants, $this->package->Header->RuntimeConstants); + $this->package->Header->RuntimeConstants = []; + $this->package->Header->RuntimeConstants = array_merge( + $selected_build_configuration->DefineConstants, + $this->project->Build->DefineConstants, + $this->package->Header->RuntimeConstants + ); $this->package->Header->CompilerExtension = $this->project->Project->Compiler; $this->package->Header->CompilerVersion = NCC_VERSION_NUMBER; - if(ncc::cliMode()) - { - Console::out('Scanning project files'); - Console::out('theseer\DirectoryScanner - Copyright (c) 2009-2014 Arne Blankerts All rights reserved.'); - } + Console::out('Scanning project files'); + Console::out('theseer\DirectoryScanner - Copyright (c) 2009-2014 Arne Blankerts All rights reserved.'); // First scan the project files and create a file struct. $DirectoryScanner = new DirectoryScanner(); @@ -121,18 +116,12 @@ // Include file components that can be compiled $DirectoryScanner->setIncludes(ComponentFileExtensions::Php); - $DirectoryScanner->setExcludes($this->selected_build_configuration->ExcludeFiles); - - // Append trailing slash to the end of the path if it's not already there - if(substr($path, -1) !== DIRECTORY_SEPARATOR) - { - $path .= DIRECTORY_SEPARATOR; - } - - $source_path = $path . $this->project->Build->SourcePath; + $DirectoryScanner->setExcludes($selected_build_configuration->ExcludeFiles); + $source_path = $this->path . $this->project->Build->SourcePath; + // TODO: Re-implement the scanning process outside the compiler, as this is will be redundant // Scan for components first. - Console::out('Scanning for components... ', false); + Console::out('Scanning for components... '); /** @var SplFileInfo $item */ /** @noinspection PhpRedundantOptionalArgumentInspection */ foreach($DirectoryScanner($source_path, True) as $item) @@ -142,22 +131,20 @@ continue; $Component = new Package\Component(); - $Component->Name = Functions::removeBasename($item->getPathname(), $path); + $Component->Name = Functions::removeBasename($item->getPathname(), $this->path); $this->package->Components[] = $Component; + + Console::outVerbose(sprintf('found component %s', $Component->Name)); } - if(ncc::cliMode()) + if(count($this->package->Components) > 0) { - if(count($this->package->Components) > 0) - { - Console::out(count($this->package->Components) . ' component(s) found'); - } - else - { - Console::out('No components found'); - } + Console::out(count($this->package->Components) . ' component(s) found'); + } + else + { + Console::out('No components found'); } - // Clear previous excludes and includes $DirectoryScanner->setExcludes([]); @@ -165,10 +152,10 @@ // Ignore component files $DirectoryScanner->setExcludes(array_merge( - $this->selected_build_configuration->ExcludeFiles, ComponentFileExtensions::Php + $selected_build_configuration->ExcludeFiles, ComponentFileExtensions::Php )); - Console::out('Scanning for resources... ', false); + Console::out('Scanning for resources... '); /** @var SplFileInfo $item */ foreach($DirectoryScanner($source_path) as $item) { @@ -177,174 +164,185 @@ continue; $Resource = new Package\Resource(); - $Resource->Name = Functions::removeBasename($item->getPathname(), $path); + $Resource->Name = Functions::removeBasename($item->getPathname(), $this->path); $this->package->Resources[] = $Resource; - } - - if(ncc::cliMode()) - { - if(count($this->package->Resources) > 0) - { - Console::out(count($this->package->Resources) . ' resources(s) found'); - } - else - { - Console::out('No resources found'); - } - } - } - - /** - * Builds the package by parsing the AST contents of the components and resources - * - * @param string $path - * @return string - * @throws BuildException - */ - public function build(string $path): string - { - if($this->package == null) - { - throw new BuildException('The prepare() method must be called before building the package'); - } - - // Append trailing slash to the end of the path if it's not already there - if(substr($path, -1) !== DIRECTORY_SEPARATOR) - { - $path .= DIRECTORY_SEPARATOR; - } - - // Runtime variables - $components = []; - $resources = []; - $processed_items = 0; - $total_items = 0; - - if(count($this->package->Components) > 0) - { - if(ncc::cliMode()) - { - Console::out('Compiling components'); - $total_items = count($this->package->Components); - } - - // Process the components and attempt to create an AST representation of the source - foreach($this->package->Components as $component) - { - if(ncc::cliMode() && $total_items > 5) - { - Console::inlineProgressBar($processed_items, $total_items); - } - - $content = file_get_contents(Functions::correctDirectorySeparator($path . $component->Name)); - $parser = (new ParserFactory())->create(ParserFactory::PREFER_PHP7); - - try - { - $stmts = $parser->parse($content); - $encoded = json_encode($stmts); - - if($encoded === false) - { - $component->DataType = ComponentDataType::b64encoded; - $component->Data = Base64::encode($content); - $component->Checksum = hash('sha1', $component->Data); - } - else - { - $component->DataType = ComponentDataType::AST; - $component->Data = json_decode($encoded, true); - $component->Checksum = null; - } - } - catch(Error $e) - { - $component->DataType = ComponentDataType::b64encoded; - $component->Data = Base64::encode($content); - $component->Checksum = hash('sha1', $component->Data); - unset($e); - } - - $component->Name = str_replace($this->project->Build->SourcePath, (string)null, $component->Name); - $components[] = $component; - $processed_items += 1; - } - - if(ncc::cliMode() && $total_items > 5) - { - print(PHP_EOL); - } - - // Update the components - $this->package->Components = $components; + Console::outVerbose(sprintf('found resource %s', $Resource->Name)); } if(count($this->package->Resources) > 0) { - // Process the resources - if(ncc::cliMode()) - { - Console::out('Processing resources'); - $processed_items = 0; - $total_items = count($this->package->Resources); - } - - foreach($this->package->Resources as $resource) - { - if(ncc::cliMode() && $total_items > 5) - { - Console::inlineProgressBar($processed_items, $total_items); - } - - // Get the data and - $resource->Data = file_get_contents(Functions::correctDirectorySeparator($path . $resource->Name)); - $resource->Data = Base64::encode($resource->Data); - $resource->Checksum = hash('sha1', $resource->Data); - $resource->Name = str_replace($this->project->Build->SourcePath, (string)null, $resource->Name); - $resources[] = $resource; - } - - // Update the resources - $this->package->Resources = $resources; + Console::out(count($this->package->Resources) . ' resources(s) found'); } + else + { + Console::out('No resources found'); + } + } - if(ncc::cliMode()) + /** + * Executes the compile process in the correct order and returns the finalized Package object + * + * @return Package|null + * @throws AccessDeniedException + * @throws BuildException + * @throws FileNotFoundException + * @throws IOException + * @throws UnsupportedRunnerException + */ + public function build(): ?Package + { + $this->compileExecutionPolicies(); + $this->compileComponents(); + $this->compileResources(); + + PackageCompiler::compilePackageConstants($this->package, [ + ConstantReferences::Assembly => $this->project->Assembly, + ConstantReferences::Build => null, + ConstantReferences::DateTime => time() + ]); + + return $this->getPackage(); + } + + /** + * Compiles the resources of the package + * + * @return void + * @throws AccessDeniedException + * @throws BuildException + * @throws FileNotFoundException + * @throws IOException + */ + public function compileResources(): void + { + if($this->package == null) + throw new BuildException('The prepare() method must be called before building the package'); + + if(count($this->package->Resources) == 0) + return; + + // Process the resources + Console::out('Processing resources'); + $total_items = count($this->package->Resources); + $processed_items = 0; + $resources = []; + + foreach($this->package->Resources as $resource) { if($total_items > 5) - print(PHP_EOL); - Console::out($this->package->Assembly->Package . ' compiled successfully'); + { + Console::inlineProgressBar($processed_items, $total_items); + } + + // Get the data and + $resource->Data = IO::fread(Functions::correctDirectorySeparator($this->path . $resource->Name)); + $resource->Data = Base64::encode($resource->Data); + $resource->Name = str_replace($this->project->Build->SourcePath, (string)null, $resource->Name); + $resource->updateChecksum(); + $resources[] = $resource; + + Console::outDebug(sprintf('processed resource %s', $resource->Name)); } - // Write the package to disk - $FileSystem = new Filesystem(); + // Update the resources + $this->package->Resources = $resources; + } - if($FileSystem->exists($path . $this->selected_build_configuration->OutputPath)) + /** + * Compiles the components of the package + * + * @return void + * @throws AccessDeniedException + * @throws BuildException + * @throws FileNotFoundException + * @throws IOException + */ + public function compileComponents(): void + { + if($this->package == null) + throw new BuildException('The prepare() method must be called before building the package'); + + if(count($this->package->Components) == 0) + return; + + Console::out('Compiling components'); + $total_items = count($this->package->Components); + $processed_items = 0; + $components = []; + + // Process the components and attempt to create an AST representation of the source + foreach($this->package->Components as $component) { + if($total_items > 5) + { + Console::inlineProgressBar($processed_items, $total_items); + } + + $content = IO::fread(Functions::correctDirectorySeparator($this->path . $component->Name)); + $parser = (new ParserFactory())->create(ParserFactory::PREFER_PHP7); + try { - $FileSystem->remove($path . $this->selected_build_configuration->OutputPath); + $stmts = $parser->parse($content); + $encoded = json_encode($stmts); + unset($stmts); + + if($encoded === false) + { + $component->DataType = ComponentDataType::b64encoded; + $component->Data = Base64::encode($content); + } + else + { + $component->DataType = ComponentDataType::AST; + $component->Data = json_decode($encoded, true); + } } - catch(IOException $e) + catch(Exception $e) { - throw new BuildException('Cannot delete directory \'' . $path . $this->selected_build_configuration->OutputPath . '\', ' . $e->getMessage(), $e); + $component->DataType = ComponentDataType::b64encoded; + $component->Data = Base64::encode($content); + unset($e); } + + unset($parser); + + $component->Name = str_replace($this->project->Build->SourcePath, (string)null, $component->Name); + $component->updateChecksum(); + $components[] = $component; + $processed_items += 1; + + Console::outDebug(sprintf('processed component %s (%s)', $component->Name, $component->DataType)); } - // Finally write the package to the disk - $FileSystem->mkdir($path . $this->selected_build_configuration->OutputPath); - $output_file = $path . $this->selected_build_configuration->OutputPath . DIRECTORY_SEPARATOR . $this->package->Assembly->Package . '.ncc'; - $FileSystem->touch($output_file); - - try + if(ncc::cliMode() && $total_items > 5) { - $this->package->save($output_file); - } - catch(Exception $e) - { - throw new BuildException('Cannot write to output file', $e); + print(PHP_EOL); } - return $output_file; + // Update the components + $this->package->Components = $components; } + + /** + * @return void + * @throws AccessDeniedException + * @throws FileNotFoundException + * @throws IOException + * @throws UnsupportedRunnerException + */ + public function compileExecutionPolicies(): void + { + PackageCompiler::compileExecutionPolicies($this->path, $this->project); + } + + /** + * @inheritDoc + */ + public function getPackage(): ?Package + { + return $this->package; + } + } \ No newline at end of file diff --git a/src/ncc/Classes/PhpExtension/Installer.php b/src/ncc/Classes/PhpExtension/Installer.php new file mode 100644 index 0000000..ade05ec --- /dev/null +++ b/src/ncc/Classes/PhpExtension/Installer.php @@ -0,0 +1,342 @@ +package = $package; + } + + /** + * Processes the given component and returns the decoded component as a string representation + * If the processed component does not result in a string representation, none will be returned. + * + * @param Component $component + * @return string|null + * @throws ComponentChecksumException + * @throws ComponentDecodeException + * @throws UnsupportedComponentTypeException + */ + public function processComponent(Package\Component $component): ?string + { + if($component->Data == null) + return null; + + if(!$component->validateChecksum()) + throw new ComponentChecksumException('Checksum validation failed for component ' . $component->Name . ', the package may be corrupted.'); + + switch($component->DataType) + { + case ComponentDataType::AST: + try + { + $stmts = $this->decodeRecursive($component->Data); + } + catch (Exception $e) + { + throw new ComponentDecodeException('Cannot decode component: ' . $component->Name . ', ' . $e->getMessage(), $e); + } + + $prettyPrinter = new Standard(); + return $prettyPrinter->prettyPrintFile($stmts); + + case ComponentDataType::b64encoded: + return Base64::decode($component->Data); + + case ComponentDataType::Plain: + return $component->Data; + + default: + throw new UnsupportedComponentTypeException('Unsupported component type \'' . $component->DataType . '\''); + } + } + + /** + * @inheritDoc + */ + public function preInstall(InstallationPaths $installationPaths): void + { + } + + /** + * @inheritDoc + */ + public function postInstall(InstallationPaths $installationPaths): void + { + $autoload_path = $installationPaths->getBinPath() . DIRECTORY_SEPARATOR . 'autoload.php'; + $autoload_src = $this->generateAutoload($installationPaths->getSourcePath(), $autoload_path); + IO::fwrite($autoload_path, $autoload_src); + } + + /** + * Processes the given resource and returns the string representation of the resource + * + * @param Package\Resource $resource + * @return string|null + * @throws ResourceChecksumException + */ + public function processResource(Package\Resource $resource): ?string + { + if(!$resource->validateChecksum()) + throw new ResourceChecksumException('Checksum validation failed for resource ' . $resource->Name . ', the package may be corrupted.'); + return Base64::decode($resource->Data); + } + + /** + * @param $value + * @return array|Comment|Node + * @throws ReflectionException + * @noinspection PhpMissingReturnTypeInspection + */ + private function decodeRecursive($value) + { + if (is_array($value)) + { + if (isset($value['nodeType'])) + { + if ($value['nodeType'] === 'Comment' || $value['nodeType'] === 'Comment_Doc') + { + return $this->decodeComment($value); + } + return $this->decodeNode($value); + } + return $this->decodeArray($value); + } + return $value; + } + + /** + * @param array $array + * @return array + * @throws ReflectionException + */ + private function decodeArray(array $array) : array + { + $decodedArray = []; + foreach ($array as $key => $value) + { + $decodedArray[$key] = $this->decodeRecursive($value); + } + return $decodedArray; + } + + /** + * @param array $value + * @return Node + * @throws ReflectionException + */ + private function decodeNode(array $value) : Node + { + $nodeType = $value['nodeType']; + if (!is_string($nodeType)) + { + throw new RuntimeException('Node type must be a string'); + } + + $reflectionClass = $this->reflectionClassFromNodeType($nodeType); + /** @var Node $node */ + $node = $reflectionClass->newInstanceWithoutConstructor(); + + if (isset($value['attributes'])) { + if (!is_array($value['attributes'])) + { + throw new RuntimeException('Attributes must be an array'); + } + + $node->setAttributes($this->decodeArray($value['attributes'])); + } + + foreach ($value as $name => $subNode) { + if ($name === 'nodeType' || $name === 'attributes') + { + continue; + } + + $node->$name = $this->decodeRecursive($subNode); + } + + return $node; + } + + /** + * @param array $value + * @return Comment + */ + private function decodeComment(array $value) : Comment + { + $className = $value['nodeType'] === 'Comment' ? Comment::class : Comment\Doc::class; + if (!isset($value['text'])) + { + throw new RuntimeException('Comment must have text'); + } + + return new $className( + $value['text'], + $value['line'] ?? -1, $value['filePos'] ?? -1, $value['tokenPos'] ?? -1, + $value['endLine'] ?? -1, $value['endFilePos'] ?? -1, $value['endTokenPos'] ?? -1 + ); + } + + /** + * @param string $nodeType + * @return ReflectionClass + * @throws ReflectionException + */ + private function reflectionClassFromNodeType(string $nodeType) : ReflectionClass + { + if (!isset($this->reflectionClassCache[$nodeType])) + { + $className = $this->classNameFromNodeType($nodeType); + $this->reflectionClassCache[$nodeType] = new ReflectionClass($className); + } + return $this->reflectionClassCache[$nodeType]; + } + + /** + * @param string $nodeType + * @return string + */ + private function classNameFromNodeType(string $nodeType) : string + { + $className = 'ncc\\ThirdParty\\nikic\\PhpParser\\Node\\' . strtr($nodeType, '_', '\\'); + if (class_exists($className)) + { + return $className; + } + + $className .= '_'; + if (class_exists($className)) + { + return $className; + } + + throw new RuntimeException("Unknown node type \"$nodeType\""); + } + + /** + * Processes the project and generates the autoloader source code. + * + * @param string $src + * @param string $output + * @return string + * @throws AccessDeniedException + * @throws CollectorException + * @throws FileNotFoundException + * @throws IOException + * @throws NoUnitsFoundException + */ + private function generateAutoload(string $src, string $output): string + { + // Construct configuration + $configuration = new Config([$src]); + $configuration->setFollowSymlinks(false); // Don't follow symlinks, it won't work on some systems. + $configuration->setTrusting(true); // Paranoid + $configuration->setOutputFile($output); + $configuration->setStaticMode(false); + // Official PHP file extensions that are missing from the default configuration (whatever) + $configuration->setInclude(ComponentFileExtensions::Php); + $configuration->setQuietMode(true); + + // Construct factory + $factory = new Factory(); + $factory->setConfig($configuration); + + // Create Collector + $result = self::runCollector($factory, $configuration); + + // Exception raises when there are no files in the project that can be processed by the autoloader + if(!$result->hasUnits()) + { + throw new NoUnitsFoundException('No units were found in the project'); + } + + $template = IO::fread($configuration->getTemplate()); + + $builder = $factory->getRenderer($result); + return $builder->render($template); + } + + /** + * Iterates through the target directories through the collector and returns the collector results. + * + * @param Factory $factory + * @param Config $config + * @return CollectorResult + * @throws CollectorException + * @throws Exception + */ + private static function runCollector(Factory $factory, Config $config): CollectorResult + { + $collector = $factory->getCollector(); + foreach($config->getDirectories() as $directory) + { + if(is_dir($directory)) + { + $scanner = $factory->getScanner()->getIterator($directory); + $collector->processDirectory($scanner); + unset($scanner); + } + else + { + $file = new SplFileInfo($directory); + $filter = $factory->getFilter(new ArrayIterator(array($file))); + foreach($filter as $file) + { + $collector->processFile($file); + } + } + } + + return $collector->getResult(); + } + } \ No newline at end of file diff --git a/src/ncc/Classes/PhpExtension/Runner.php b/src/ncc/Classes/PhpExtension/Runner.php new file mode 100644 index 0000000..fe8dbb4 --- /dev/null +++ b/src/ncc/Classes/PhpExtension/Runner.php @@ -0,0 +1,67 @@ +Execute->Target = null; + $execution_unit->ExecutionPolicy = $policy; + $execution_unit->Data = Base64::encode(IO::fread($target_file)); + + return $execution_unit; + } + + /** + * Returns the file extension to use for the target file + * + * @return string + */ + public static function getFileExtension(): string + { + return '.php'; + } + + /** + * @param ExecutionPointer $pointer + * @return Process + * @throws RunnerExecutionException + */ + public static function prepareProcess(ExecutionPointer $pointer): Process + { + $php_bin = new ExecutableFinder(); + $php_bin = $php_bin->find('php'); + if($php_bin == null) + throw new RunnerExecutionException('Cannot locate PHP executable'); + + if($pointer->ExecutionPolicy->Execute->Options !== null && count($pointer->ExecutionPolicy->Execute->Options) > 0) + return new Process(array_merge([$php_bin, $pointer->FilePointer], $pointer->ExecutionPolicy->Execute->Options)); + return new Process([$php_bin, $pointer->FilePointer]); + } + } \ No newline at end of file diff --git a/src/ncc/Exceptions/ComponentChecksumException.php b/src/ncc/Exceptions/ComponentChecksumException.php new file mode 100644 index 0000000..68bb520 --- /dev/null +++ b/src/ncc/Exceptions/ComponentChecksumException.php @@ -0,0 +1,28 @@ +message = $message; + $this->previous = $previous; + } + } \ No newline at end of file diff --git a/src/ncc/Exceptions/ComponentDecodeException.php b/src/ncc/Exceptions/ComponentDecodeException.php new file mode 100644 index 0000000..593255f --- /dev/null +++ b/src/ncc/Exceptions/ComponentDecodeException.php @@ -0,0 +1,28 @@ +message = $message; + $this->previous = $previous; + } + } \ No newline at end of file diff --git a/src/ncc/Exceptions/ExecutionUnitNotFoundException.php b/src/ncc/Exceptions/ExecutionUnitNotFoundException.php new file mode 100644 index 0000000..228f512 --- /dev/null +++ b/src/ncc/Exceptions/ExecutionUnitNotFoundException.php @@ -0,0 +1,8 @@ +message = $message; + $this->previous = $previous; + } + } \ No newline at end of file diff --git a/src/ncc/Exceptions/InstallationException.php b/src/ncc/Exceptions/InstallationException.php new file mode 100644 index 0000000..1b99c67 --- /dev/null +++ b/src/ncc/Exceptions/InstallationException.php @@ -0,0 +1,28 @@ +message = $message; + $this->previous = $previous; + } + } \ No newline at end of file diff --git a/src/ncc/Exceptions/InvalidExecutionPolicyName.php b/src/ncc/Exceptions/InvalidExecutionPolicyName.php new file mode 100644 index 0000000..2699960 --- /dev/null +++ b/src/ncc/Exceptions/InvalidExecutionPolicyName.php @@ -0,0 +1,14 @@ +message = $message; + $this->previous = $previous; + } + } \ No newline at end of file diff --git a/src/ncc/Exceptions/PackageAlreadyInstalledException.php b/src/ncc/Exceptions/PackageAlreadyInstalledException.php new file mode 100644 index 0000000..fbf5c49 --- /dev/null +++ b/src/ncc/Exceptions/PackageAlreadyInstalledException.php @@ -0,0 +1,28 @@ +message = $message; + $this->previous = $previous; + } + } \ No newline at end of file diff --git a/src/ncc/Exceptions/PackageLockException.php b/src/ncc/Exceptions/PackageLockException.php new file mode 100644 index 0000000..b86c20c --- /dev/null +++ b/src/ncc/Exceptions/PackageLockException.php @@ -0,0 +1,28 @@ +message = $message; + $this->previous = $previous; + } + } \ No newline at end of file diff --git a/src/ncc/Exceptions/PackageNotFoundException.php b/src/ncc/Exceptions/PackageNotFoundException.php new file mode 100644 index 0000000..88bd370 --- /dev/null +++ b/src/ncc/Exceptions/PackageNotFoundException.php @@ -0,0 +1,28 @@ +message = $message; + $this->previous = $previous; + } + } \ No newline at end of file diff --git a/src/ncc/Exceptions/PackageParsingException.php b/src/ncc/Exceptions/PackageParsingException.php new file mode 100644 index 0000000..70c1be7 --- /dev/null +++ b/src/ncc/Exceptions/PackageParsingException.php @@ -0,0 +1,28 @@ +message = $message; + $this->previous = $previous; + } + } \ No newline at end of file diff --git a/src/ncc/Exceptions/ProjectConfigurationNotFoundException.php b/src/ncc/Exceptions/ProjectConfigurationNotFoundException.php new file mode 100644 index 0000000..2897f72 --- /dev/null +++ b/src/ncc/Exceptions/ProjectConfigurationNotFoundException.php @@ -0,0 +1,13 @@ +message = $message; + $this->code = $code; + $this->previous = $previous; + } + } \ No newline at end of file diff --git a/src/ncc/Exceptions/RunnerExecutionException.php b/src/ncc/Exceptions/RunnerExecutionException.php new file mode 100644 index 0000000..e568dd1 --- /dev/null +++ b/src/ncc/Exceptions/RunnerExecutionException.php @@ -0,0 +1,28 @@ +message = $message; + $this->previous = $previous; + } + } \ No newline at end of file diff --git a/src/ncc/Exceptions/UndefinedExecutionPolicyException.php b/src/ncc/Exceptions/UndefinedExecutionPolicyException.php new file mode 100644 index 0000000..8797b5e --- /dev/null +++ b/src/ncc/Exceptions/UndefinedExecutionPolicyException.php @@ -0,0 +1,28 @@ +message = $message; + $this->previous = $previous; + } + } \ No newline at end of file diff --git a/src/ncc/Exceptions/UnsupportedComponentTypeException.php b/src/ncc/Exceptions/UnsupportedComponentTypeException.php new file mode 100644 index 0000000..ecd4c79 --- /dev/null +++ b/src/ncc/Exceptions/UnsupportedComponentTypeException.php @@ -0,0 +1,28 @@ +message = $message; + $this->previous = $previous; + } + } \ No newline at end of file diff --git a/src/ncc/Exceptions/UnsupportedRunnerException.php b/src/ncc/Exceptions/UnsupportedRunnerException.php new file mode 100644 index 0000000..830a7a1 --- /dev/null +++ b/src/ncc/Exceptions/UnsupportedRunnerException.php @@ -0,0 +1,11 @@ +message = $message; + $this->previous = $previous; + } + } \ No newline at end of file diff --git a/src/ncc/Interfaces/CompilerInterface.php b/src/ncc/Interfaces/CompilerInterface.php index 4fa160d..c098abf 100644 --- a/src/ncc/Interfaces/CompilerInterface.php +++ b/src/ncc/Interfaces/CompilerInterface.php @@ -3,23 +3,79 @@ namespace ncc\Interfaces; use ncc\Abstracts\Options\BuildConfigurationValues; + use ncc\Exceptions\AccessDeniedException; + use ncc\Exceptions\BuildException; + use ncc\Exceptions\FileNotFoundException; + use ncc\Exceptions\IOException; + use ncc\Exceptions\UnsupportedRunnerException; + use ncc\Objects\Package; + use ncc\Objects\ProjectConfiguration; interface CompilerInterface { + /** + * Public constructor + * + * @param ProjectConfiguration $project + * @param string $path + */ + public function __construct(ProjectConfiguration $project, string $path); + /** * Prepares the package for the build process, this method is called before build() * - * @param string $path The path that the project file is located in (project.json) * @param string $build_configuration The build configuration to use to build the project * @return void */ - public function prepare(string $path, string $build_configuration=BuildConfigurationValues::DefaultConfiguration): void; + public function prepare(string $build_configuration=BuildConfigurationValues::DefaultConfiguration): void; /** - * Builds the package, returns the output path of the build + * Executes the compile process in the correct order and returns the finalized Package object * - * @param string $path The path that the project file is located in (project.json) - * @return string Returns the output path of the build + * @return Package|null + * @throws AccessDeniedException + * @throws BuildException + * @throws FileNotFoundException + * @throws IOException + * @throws UnsupportedRunnerException */ - public function build(string $path): string; + public function build(): ?Package; + + /** + * Compiles the components of the package + * + * @return void + * @throws AccessDeniedException + * @throws FileNotFoundException + * @throws IOException + */ + public function compileComponents(): void; + + /** + * Compiles the resources of the package + * + * @return void + * @throws AccessDeniedException + * @throws FileNotFoundException + * @throws IOException + */ + public function compileResources(): void; + + /** + * Compiles the execution policies of the package + * + * @return void + * @throws AccessDeniedException + * @throws FileNotFoundException + * @throws IOException + * @throws UnsupportedRunnerException + */ + public function compileExecutionPolicies(): void; + + /** + * Returns the current state of the package + * + * @return Package|null + */ + public function getPackage(): ?Package; } \ No newline at end of file diff --git a/src/ncc/Interfaces/InstallerInterface.php b/src/ncc/Interfaces/InstallerInterface.php new file mode 100644 index 0000000..75049ce --- /dev/null +++ b/src/ncc/Interfaces/InstallerInterface.php @@ -0,0 +1,59 @@ +CredentialsPath = PathFinder::getDataPath(Scopes::System) . DIRECTORY_SEPARATOR . 'credentials.store'; } @@ -51,7 +55,7 @@ * * @return void * @throws AccessDeniedException - * @throws RuntimeException + * @throws IOException */ public function constructStore(): void { @@ -68,12 +72,7 @@ $VaultObject = new Vault(); $VaultObject->Version = Versions::CredentialsStoreVersion; - if(!@file_put_contents($this->CredentialsPath, ZiProto::encode($VaultObject->toArray()))) - { - throw new RuntimeException('Cannot create file \'' . $this->CredentialsPath . '\''); - } - - chmod($this->CredentialsPath, 0600); + IO::fwrite($this->CredentialsPath, ZiProto::encode($VaultObject->toArray()), 0600); } /** @@ -81,6 +80,7 @@ * * @return Vault * @throws AccessDeniedException + * @throws IOException * @throws RuntimeException */ public function getVault(): Vault @@ -94,17 +94,15 @@ try { - $Vault = ZiProto::decode(file_get_contents($this->CredentialsPath)); + $Vault = ZiProto::decode(IO::fread($this->CredentialsPath)); } - catch(\Exception $e) + catch(Exception $e) { // TODO: Implement error-correction for corrupted credentials store. throw new RuntimeException($e->getMessage(), $e); } - $Vault = Vault::fromArray($Vault); - - return $Vault; + return Vault::fromArray($Vault); } /** @@ -113,15 +111,16 @@ * @param Vault $vault * @return void * @throws AccessDeniedException + * @throws IOException */ - public function saveVault(Vault $vault) + public function saveVault(Vault $vault): void { if(!$this->checkAccess()) { throw new AccessDeniedException('Cannot write to credentials store without system permissions'); } - file_put_contents($this->CredentialsPath, ZiProto::encode($vault->toArray())); + IO::fwrite($this->CredentialsPath, ZiProto::encode($vault->toArray()), 0600); } /** @@ -132,8 +131,9 @@ * @throws AccessDeniedException * @throws InvalidCredentialsEntryException * @throws RuntimeException + * @throws IOException */ - public function registerEntry(Vault\Entry $entry) + public function registerEntry(Vault\Entry $entry): void { if(!preg_match('/^[\w-]+$/', $entry->Alias)) { @@ -152,7 +152,7 @@ /** * @return null */ - public function getCredentialsPath() + public function getCredentialsPath(): ?string { return $this->CredentialsPath; } diff --git a/src/ncc/Managers/ExecutionPointerManager.php b/src/ncc/Managers/ExecutionPointerManager.php new file mode 100644 index 0000000..bedd2b5 --- /dev/null +++ b/src/ncc/Managers/ExecutionPointerManager.php @@ -0,0 +1,424 @@ +cleanTemporaryUnits(); + } + catch(Exception $e) + { + unset($e); + } + } + + /** + * @throws InvalidScopeException + */ + public function __construct() + { + $this->RunnerPath = PathFinder::getRunnerPath(Scopes::System); + $this->TemporaryUnits = []; + } + + /** + * Deletes all temporary files and directories + * + * @return void + */ + public function cleanTemporaryUnits(): void + { + if(count($this->TemporaryUnits) == 0) + return; + + try + { + foreach($this->TemporaryUnits as $datum) + { + $this->removeUnit($datum['package'], $datum['version'], $datum['name']); + } + } + catch(Exception $e) + { + unset($e); + } + } + + /** + * Calculates the Package ID for the execution pointers + * + * @param string $package + * @param string $version + * @return string + */ + private function getPackageId(string $package, string $version): string + { + return hash('haval128,4', $package . $version); + } + + /** + * Adds a new Execution Unit to the + * + * @param string $package + * @param string $version + * @param ExecutionUnit $unit + * @param bool $temporary + * @return void + * @throws AccessDeniedException + * @throws FileNotFoundException + * @throws IOException + * @throws UnsupportedRunnerException + * @noinspection PhpUnused + */ + public function addUnit(string $package, string $version, ExecutionUnit $unit, bool $temporary=false): void + { + if(Resolver::resolveScope() !== Scopes::System) + throw new AccessDeniedException('Cannot add new ExecutionUnit \'' . $unit->ExecutionPolicy->Name .'\' for ' . $package . ', insufficient permissions'); + + $package_id = $this->getPackageId($package, $version); + $package_config_path = $this->RunnerPath . DIRECTORY_SEPARATOR . $package_id . '.inx'; + $package_bin_path = $this->RunnerPath . DIRECTORY_SEPARATOR . $package_id; + + $filesystem = new Filesystem(); + + // Either load or create the pointers file + if(!$filesystem->exists($package_config_path)) + { + $execution_pointers = new ExecutionPointers($package, $version); + } + else + { + $execution_pointers = ExecutionPointers::fromArray(ZiProto::decode(IO::fread($package_config_path))); + } + + $bin_file = $package_bin_path . DIRECTORY_SEPARATOR . hash('haval128,4', $unit->ExecutionPolicy->Name); + $bin_file .= match ($unit->ExecutionPolicy->Runner) { + Runners::php => Runner::getFileExtension(), + default => throw new UnsupportedRunnerException('The runner \'' . $unit->ExecutionPolicy->Runner . '\' is not supported'), + }; + + if($filesystem->exists($bin_file) && $temporary) + return; + + if(!$filesystem->exists($package_bin_path)) + $filesystem->mkdir($package_bin_path); + + if($filesystem->exists($bin_file)) + $filesystem->remove($bin_file); + + IO::fwrite($bin_file, $unit->Data); + $execution_pointers->addUnit($unit, $bin_file); + IO::fwrite($package_config_path, ZiProto::encode($execution_pointers->toArray(true))); + + if($temporary) + { + $this->TemporaryUnits[] = [ + 'package' => $package, + 'version' => $version, + 'unit' => $unit->ExecutionPolicy->Name + ]; + } + } + + /** + * Deletes and removes the installed unit + * + * @param string $package + * @param string $version + * @param string $name + * @return bool + * @throws AccessDeniedException + * @throws FileNotFoundException + * @throws IOException + */ + public function removeUnit(string $package, string $version, string $name): bool + { + if(Resolver::resolveScope() !== Scopes::System) + throw new AccessDeniedException('Cannot remove ExecutionUnit \'' . $name .'\' for ' . $package . ', insufficient permissions'); + + $package_id = $this->getPackageId($package, $version); + $package_config_path = $this->RunnerPath . DIRECTORY_SEPARATOR . $package_id . '.inx'; + $package_bin_path = $this->RunnerPath . DIRECTORY_SEPARATOR . $package_id; + + $filesystem = new Filesystem(); + if(!$filesystem->exists($package_config_path)) + return false; + $execution_pointers = ExecutionPointers::fromArray(ZiProto::decode(IO::fread($package_config_path))); + $unit = $execution_pointers->getUnit($name); + if($unit == null) + return false; + $results = $execution_pointers->deleteUnit($name); + + // Delete everything if there are no execution pointers configured + if(count($execution_pointers->getPointers()) == 0) + { + $filesystem->remove($package_config_path); + $filesystem->remove($package_bin_path); + + return $results; + } + + // Delete the single execution pointer file + if($filesystem->exists($unit->FilePointer)) + $filesystem->remove($unit->FilePointer); + + return $results; + } + + /** + * Returns an array of configured units for a package version + * + * @param string $package + * @param string $version + * @return array + * @throws AccessDeniedException + * @throws FileNotFoundException + * @throws IOException + * @noinspection PhpUnused + */ + public function getUnits(string $package, string $version): array + { + $package_id = $this->getPackageId($package, $version); + $package_config_path = $this->RunnerPath . DIRECTORY_SEPARATOR . $package_id . '.inx'; + + if(!file_exists($package_config_path)) + return []; + + $execution_pointers = ExecutionPointers::fromArray(ZiProto::decode(IO::fread($package_config_path))); + $results = []; + foreach($execution_pointers->getPointers() as $pointer) + { + $results[] = $pointer->ExecutionPolicy->Name; + } + + return $results; + } + + /** + * Executes a unit + * + * @param string $package + * @param string $version + * @param string $name + * @return void + * @throws AccessDeniedException + * @throws ExecutionUnitNotFoundException + * @throws FileNotFoundException + * @throws IOException + * @throws NoAvailableUnitsException + * @throws UnsupportedRunnerException + * @throws RunnerExecutionException + */ + public function executeUnit(string $package, string $version, string $name): void + { + $package_id = $this->getPackageId($package, $version); + $package_config_path = $this->RunnerPath . DIRECTORY_SEPARATOR . $package_id . '.inx'; + + if(!file_exists($package_config_path)) + throw new NoAvailableUnitsException('There is no available units for \'' . $package . '=' .$version .'\''); + + $execution_pointers = ExecutionPointers::fromArray(ZiProto::decode(IO::fread($package_config_path))); + $unit = $execution_pointers->getUnit($name); + + if($unit == null) + throw new ExecutionUnitNotFoundException('The execution unit \'' . $name . '\' was not found for \'' . $package . '=' .$version .'\''); + + $process = match (strtolower($unit->ExecutionPolicy->Runner)) + { + Runners::php => Runner::prepareProcess($unit), + default => throw new UnsupportedRunnerException('The runner \'' . $unit->ExecutionPolicy->Runner . '\' is not supported'), + }; + + if($unit->ExecutionPolicy->Execute->WorkingDirectory !== null) + $process->setWorkingDirectory($unit->ExecutionPolicy->Execute->WorkingDirectory); + if($unit->ExecutionPolicy->Execute->Timeout !== null) + $process->setTimeout((float)$unit->ExecutionPolicy->Execute->Timeout); + + if($unit->ExecutionPolicy->Execute->Silent) + { + $process->disableOutput(); + $process->setTty(false); + } + elseif($unit->ExecutionPolicy->Execute->Tty) + { + $process->enableOutput(); + $process->setTty(true); + } + else + { + $process->enableOutput(); + } + + try + { + if($unit->ExecutionPolicy->Message !== null) + Console::out($unit->ExecutionPolicy->Message); + + $process->run(function ($type, $buffer) { + Console::out($buffer); + }); + + $process->wait(); + } + catch(Exception $e) + { + unset($e); + $this->handleExit($package, $version, $unit->ExecutionPolicy->ExitHandlers->Error); + } + + if($unit->ExecutionPolicy->ExitHandlers !== null) + { + if($process->isSuccessful() && $unit->ExecutionPolicy->ExitHandlers->Success !== null) + { + $this->handleExit($package, $version, $unit->ExecutionPolicy->ExitHandlers->Success); + } + elseif($process->isSuccessful() && $unit->ExecutionPolicy->ExitHandlers->Error !== null) + { + $this->handleExit($package, $version, $unit->ExecutionPolicy->ExitHandlers->Error); + } + else + { + $this->handleExit($package, $version, $unit->ExecutionPolicy->ExitHandlers->Success, $process); + $this->handleExit($package, $version, $unit->ExecutionPolicy->ExitHandlers->Warning, $process); + $this->handleExit($package, $version, $unit->ExecutionPolicy->ExitHandlers->Error, $process); + } + } + } + + /** + * Temporarily executes a + * + * @param Package $package + * @param string $unit_name + * @return void + * @throws AccessDeniedException + * @throws ExecutionUnitNotFoundException + * @throws FileNotFoundException + * @throws IOException + * @throws NoAvailableUnitsException + * @throws RunnerExecutionException + * @throws UnsupportedRunnerException + */ + public function temporaryExecute(Package $package, string $unit_name): void + { + // First get the execution unit from the package. + $unit = $package->getExecutionUnit($unit_name); + + // Get the required units + $required_units = []; + if($unit->ExecutionPolicy->ExitHandlers !== null) + { + $required_unit = $unit->ExecutionPolicy?->ExitHandlers?->Success?->Run; + if($required_unit !== null) + $required_units[] = $required_unit; + + $required_unit = $unit->ExecutionPolicy?->ExitHandlers?->Warning?->Run; + if($required_unit !== null) + $required_units[] = $required_unit; + + $required_unit = $unit->ExecutionPolicy?->ExitHandlers?->Error?->Run; + if($required_unit !== null) + $required_units = $required_unit; + } + + // Install the units temporarily + $this->addUnit($package->Assembly->Package, $package->Assembly->Version, $unit, true); + foreach($required_units as $r_unit) + { + $this->addUnit($package->Assembly->Package, $package->Assembly->Version, $r_unit, true); + } + + $this->executeUnit($package->Assembly->Package, $package->Assembly->Version, $unit_name); + $this->cleanTemporaryUnits(); + } + + /** + * Handles an exit handler object. + * + * If Process is Null and EndProcess is true, the method will end the process + * if Process is not Null the exit handler will only execute if the process' exit code is the same + * + * @param string $package + * @param string $version + * @param ExitHandle $exitHandle + * @param Process|null $process + * @return bool + * @throws AccessDeniedException + * @throws ExecutionUnitNotFoundException + * @throws FileNotFoundException + * @throws IOException + * @throws NoAvailableUnitsException + * @throws RunnerExecutionException + * @throws UnsupportedRunnerException + */ + public function handleExit(string $package, string $version, ExitHandle $exitHandle, ?Process $process=null): bool + { + if($exitHandle->Message !== null) + Console::out($exitHandle->Message); + + if($process !== null && !$exitHandle->EndProcess) + { + if($exitHandle->ExitCode !== $process->getExitCode()) + return false; + } + elseif($exitHandle->EndProcess) + { + exit($exitHandle->ExitCode); + } + + if($exitHandle->Run !== null) + { + $this->executeUnit($package, $version, $exitHandle->Run); + } + + return true; + } + + } \ No newline at end of file diff --git a/src/ncc/Managers/PackageLockManager.php b/src/ncc/Managers/PackageLockManager.php new file mode 100644 index 0000000..4c7d6cb --- /dev/null +++ b/src/ncc/Managers/PackageLockManager.php @@ -0,0 +1,152 @@ +PackageLockPath = PathFinder::getPackageLock(Scopes::System); + + try + { + $this->load(); + } + catch (PackageLockException $e) + { + unset($e); + } + } + + /** + * Loads the PackageLock from the disk + * + * @return void + * @throws PackageLockException + */ + public function load(): void + { + if(RuntimeCache::get($this->PackageLockPath) !== null) + { + $this->PackageLock = RuntimeCache::get($this->PackageLockPath); + return; + } + + if(file_exists($this->PackageLockPath) && is_file($this->PackageLockPath)) + { + try + { + Console::outDebug('reading package lock file'); + $data = IO::fread($this->PackageLockPath); + if(strlen($data) > 0) + { + $this->PackageLock = PackageLock::fromArray(ZiProto::decode($data)); + } + else + { + $this->PackageLock = new PackageLock(); + } + } + catch(Exception $e) + { + throw new PackageLockException('The PackageLock file cannot be parsed', $e); + } + } + else + { + $this->PackageLock = new PackageLock(); + } + + RuntimeCache::set($this->PackageLockPath, $this->PackageLock); + } + + /** + * Saves the PackageLock to disk + * + * @return void + * @throws AccessDeniedException + * @throws PackageLockException + */ + public function save(): void + { + // Don't save something that isn't loaded lol + if($this->PackageLock == null) + return; + + if(Resolver::resolveScope() !== Scopes::System) + throw new AccessDeniedException('Cannot write to PackageLock, insufficient permissions'); + + try + { + IO::fwrite($this->PackageLockPath, ZiProto::encode($this->PackageLock->toArray(true)), 0755); + RuntimeCache::set($this->PackageLockPath, $this->PackageLock); + } + catch(IOException $e) + { + throw new PackageLockException('Cannot save the package lock file to disk', $e); + } + } + + /** + * Constructs the package lock file if it doesn't exist + * + * @return void + * @throws AccessDeniedException + * @throws PackageLockException + */ + public function constructLockFile(): void + { + try + { + $this->load(); + } + catch (PackageLockException $e) + { + unset($e); + $this->PackageLock = new PackageLock(); + } + + $this->save(); + } + + /** + * @return PackageLock|null + * @throws PackageLockException + */ + public function getPackageLock(): ?PackageLock + { + if($this->PackageLock == null) + $this->load(); + return $this->PackageLock; + } + } \ No newline at end of file diff --git a/src/ncc/Managers/PackageManager.php b/src/ncc/Managers/PackageManager.php new file mode 100644 index 0000000..3719e2b --- /dev/null +++ b/src/ncc/Managers/PackageManager.php @@ -0,0 +1,490 @@ +PackagesPath = PathFinder::getPackagesPath(Scopes::System); + $this->PackageLockManager = new PackageLockManager(); + $this->PackageLockManager->load(); + } + + /** + * Installs a local package onto the system + * + * @param string $input + * @return string + * @throws AccessDeniedException + * @throws FileNotFoundException + * @throws IOException + * @throws InstallationException + * @throws PackageAlreadyInstalledException + * @throws PackageLockException + * @throws PackageParsingException + * @throws UnsupportedCompilerExtensionException + * @throws UnsupportedRunnerException + * @throws VersionNotFoundException + */ + public function install(string $input): string + { + if(Resolver::resolveScope() !== Scopes::System) + throw new AccessDeniedException('Insufficient permission to install packages'); + + Console::outVerbose(sprintf('Installing %s', $input)); + if(!file_exists($input) || !is_file($input) || !is_readable($input)) + throw new FileNotFoundException('The specified file \'' . $input .' \' does not exist or is not readable.'); + + $package = Package::load($input); + + if($this->getPackageVersion($package->Assembly->Package, $package->Assembly->Version) !== null) + throw new PackageAlreadyInstalledException('The package ' . $package->Assembly->Package . '==' . $package->Assembly->Version . ' is already installed'); + + $extension = $package->Header->CompilerExtension->Extension; + $installation_paths = new InstallationPaths($this->PackagesPath . DIRECTORY_SEPARATOR . $package->Assembly->Package . '==' . $package->Assembly->Version); + $installer = match ($extension) { + CompilerExtensions::PHP => new Installer($package), + default => throw new UnsupportedCompilerExtensionException('The compiler extension \'' . $extension . '\' is not supported'), + }; + $execution_pointer_manager = new ExecutionPointerManager(); + PackageCompiler::compilePackageConstants($package, [ + ConstantReferences::Install => $installation_paths + ]); + + Console::outVerbose(sprintf('Successfully parsed %s', $package->Assembly->Package)); + + if(Resolver::checkLogLevel(LogLevel::Debug, Main::getLogLevel())) + { + Console::outDebug(sprintf('installer.install_path: %s', $installation_paths->getInstallationPath())); + Console::outDebug(sprintf('installer.data_path: %s', $installation_paths->getDataPath())); + Console::outDebug(sprintf('installer.bin_path: %s', $installation_paths->getBinPath())); + Console::outDebug(sprintf('installer.src_path: %s', $installation_paths->getSourcePath())); + + foreach($package->Assembly->toArray() as $prop => $value) + Console::outDebug(sprintf('assembly.%s: %s', $prop, ($value ?? 'n/a'))); + foreach($package->Header->CompilerExtension->toArray() as $prop => $value) + Console::outDebug(sprintf('header.compiler.%s: %s', $prop, ($value ?? 'n/a'))); + } + + Console::out('Installing ' . $package->Assembly->Package); + + // 4 For Directory Creation, preInstall, postInstall & initData methods + $steps = (4 + count($package->Components) + count ($package->Resources) + count ($package->ExecutionUnits)); + + // Include the Execution units + if($package->Installer?->PreInstall !== null) + $steps += count($package->Installer->PreInstall); + if($package->Installer?->PostInstall!== null) + $steps += count($package->Installer->PostInstall); + + $current_steps = 0; + $filesystem = new Filesystem(); + + try + { + $filesystem->mkdir($installation_paths->getInstallationPath(), 0755); + $filesystem->mkdir($installation_paths->getBinPath(), 0755); + $filesystem->mkdir($installation_paths->getDataPath(), 0755); + $filesystem->mkdir($installation_paths->getSourcePath(), 0755); + $current_steps += 1; + Console::inlineProgressBar($current_steps, $steps); + } + catch(Exception $e) + { + throw new InstallationException('Error while creating directory, ' . $e->getMessage(), $e); + } + + try + { + self::initData($package, $installation_paths); + Console::outDebug(sprintf('saving shadow package to %s', $installation_paths->getDataPath() . DIRECTORY_SEPARATOR . 'pkg')); + $package->save($installation_paths->getDataPath() . DIRECTORY_SEPARATOR . 'pkg'); + $current_steps += 1; + Console::inlineProgressBar($current_steps, $steps); + } + catch(Exception $e) + { + throw new InstallationException('Cannot initialize package install, ' . $e->getMessage(), $e); + } + + // Execute the pre-installation stage before the installation stage + try + { + $installer->preInstall($installation_paths); + $current_steps += 1; + Console::inlineProgressBar($current_steps, $steps); + } + catch (Exception $e) + { + throw new InstallationException('Pre installation stage failed, ' . $e->getMessage(), $e); + } + + if($package->Installer?->PreInstall !== null && count($package->Installer->PreInstall) > 0) + { + foreach($package->Installer->PreInstall as $unit_name) + { + try + { + $execution_pointer_manager->temporaryExecute($package, $unit_name); + } + catch(Exception $e) + { + Console::outWarning('Cannot execute unit ' . $unit_name . ', ' . $e->getMessage()); + } + + $current_steps += 1; + Console::inlineProgressBar($current_steps, $steps); + } + } + + // Process & Install the components + foreach($package->Components as $component) + { + Console::outDebug(sprintf('processing component %s (%s)', $component->Name, $component->DataType)); + + try + { + $data = $installer->processComponent($component); + if($data !== null) + { + $component_path = $installation_paths->getSourcePath() . DIRECTORY_SEPARATOR . $component->Name; + $component_dir = dirname($component_path); + if(!$filesystem->exists($component_dir)) + $filesystem->mkdir($component_dir); + IO::fwrite($component_path, $data); + } + } + catch(Exception $e) + { + throw new InstallationException('Cannot process one or more components, ' . $e->getMessage(), $e); + } + + $current_steps += 1; + Console::inlineProgressBar($current_steps, $steps); + } + + // Process & Install the resources + foreach($package->Resources as $resource) + { + Console::outDebug(sprintf('processing resource %s', $resource->Name)); + + try + { + $data = $installer->processResource($resource); + if($data !== null) + { + $resource_path = $installation_paths->getSourcePath() . DIRECTORY_SEPARATOR . $resource->Name; + $resource_dir = dirname($resource_path); + if(!$filesystem->exists($resource_dir)) + $filesystem->mkdir($resource_dir); + IO::fwrite($resource_path, $data); + } + } + catch(Exception $e) + { + throw new InstallationException('Cannot process one or more resources, ' . $e->getMessage(), $e); + } + + $current_steps += 1; + Console::inlineProgressBar($current_steps, $steps); + } + + // Install execution units + // TODO: Implement symlink support + if(count($package->ExecutionUnits) > 0) + { + $execution_pointer_manager = new ExecutionPointerManager(); + $unit_paths = []; + + foreach($package->ExecutionUnits as $executionUnit) + { + $execution_pointer_manager->addUnit($package->Assembly->Package, $package->Assembly->Version, $executionUnit); + $current_steps += 1; + Console::inlineProgressBar($current_steps, $steps); + } + + IO::fwrite($installation_paths->getDataPath() . DIRECTORY_SEPARATOR . 'exec', ZiProto::encode($unit_paths)); + } + + // Execute the post-installation stage after the installation is complete + try + { + $installer->postInstall($installation_paths); + $current_steps += 1; + Console::inlineProgressBar($current_steps, $steps); + } + catch (Exception $e) + { + throw new InstallationException('Post installation stage failed, ' . $e->getMessage(), $e); + } + + if($package->Installer?->PostInstall !== null && count($package->Installer->PostInstall) > 0) + { + foreach($package->Installer->PostInstall as $unit_name) + { + try + { + $execution_pointer_manager->temporaryExecute($package, $unit_name); + } + catch(Exception $e) + { + Console::outWarning('Cannot execute unit ' . $unit_name . ', ' . $e->getMessage()); + } + + $current_steps += 1; + Console::inlineProgressBar($current_steps, $steps); + } + } + + $this->getPackageLockManager()->getPackageLock()->addPackage($package, $installation_paths->getInstallationPath()); + $this->getPackageLockManager()->save(); + + return $package->Assembly->Package; + } + + /** + * Returns an existing package entry, returns null if no such entry exists + * + * @param string $package + * @return PackageEntry|null + * @throws PackageLockException + * @throws PackageLockException + */ + public function getPackage(string $package): ?PackageEntry + { + return $this->getPackageLockManager()->getPackageLock()->getPackage($package); + } + + /** + * Returns an existing version entry, returns null if no such entry exists + * + * @param string $package + * @param string $version + * @return VersionEntry|null + * @throws VersionNotFoundException + * @throws PackageLockException + */ + public function getPackageVersion(string $package, string $version): ?VersionEntry + { + return $this->getPackage($package)?->getVersion($version); + } + + /** + * Returns the latest version of the package, or null if there is no entry + * + * @param string $package + * @return VersionEntry|null + * @throws VersionNotFoundException + * @throws PackageLockException + */ + public function getLatestVersion(string $package): ?VersionEntry + { + return $this->getPackage($package)?->getVersion($this->getPackage($package)?->getLatestVersion()); + } + + /** + * Returns an array of all packages and their installed versions + * + * @return array + * @throws PackageLockException + * @throws PackageLockException + */ + public function getInstalledPackages(): array + { + return $this->getPackageLockManager()->getPackageLock()->getPackages(); + } + + /** + * Uninstalls a package version + * + * @param string $package + * @param string $version + * @return void + * @throws AccessDeniedException + * @throws FileNotFoundException + * @throws IOException + * @throws PackageLockException + * @throws PackageNotFoundException + * @throws VersionNotFoundException + */ + public function uninstallPackageVersion(string $package, string $version): void + { + if(Resolver::resolveScope() !== Scopes::System) + throw new AccessDeniedException('Insufficient permission to uninstall packages'); + + $version_entry = $this->getPackageVersion($package, $version); + if($version_entry == null) + throw new PackageNotFoundException(sprintf('The package %s==%s was not found', $package, $version)); + + Console::out(sprintf('Uninstalling %s==%s', $package, $version)); + Console::outVerbose(sprintf('Removing package %s==%s from PackageLock', $package, $version)); + if(!$this->getPackageLockManager()->getPackageLock()->removePackageVersion($package, $version)) + Console::outDebug('warning: removing package from package lock failed'); + + $this->getPackageLockManager()->save(); + + Console::outVerbose('Removing package files'); + $scanner = new DirectoryScanner(); + $filesystem = new Filesystem(); + + /** @var SplFileInfo $item */ + /** @noinspection PhpRedundantOptionalArgumentInspection */ + foreach($scanner($version_entry->Location, true) as $item) + { + if(is_file($item->getPath())) + { + Console::outDebug(sprintf('deleting %s', $item->getPath())); + $filesystem->remove($item->getPath()); + } + } + + $filesystem->remove($version_entry->Location); + + if($version_entry->ExecutionUnits !== null && count($version_entry->ExecutionUnits) > 0) + { + Console::outVerbose('Uninstalling execution units'); + + $execution_pointer_manager = new ExecutionPointerManager(); + foreach($version_entry->ExecutionUnits as $executionUnit) + { + if(!$execution_pointer_manager->removeUnit($package, $version, $executionUnit->ExecutionPolicy->Name)) + Console::outDebug(sprintf('warning: removing execution unit %s failed', $executionUnit->ExecutionPolicy->Name)); + } + } + } + + /** + * Uninstalls all versions of a package + * + * @param string $package + * @return void + * @throws AccessDeniedException + * @throws PackageLockException + * @throws PackageNotFoundException + * @throws VersionNotFoundException + */ + public function uninstallPackage(string $package): void + { + if(Resolver::resolveScope() !== Scopes::System) + throw new AccessDeniedException('Insufficient permission to uninstall packages'); + + $package_entry = $this->getPackage($package); + if($package_entry == null) + throw new PackageNotFoundException(sprintf('The package %s was not found', $package)); + + foreach($package_entry->getVersions() as $version) + { + $version_entry = $package_entry->getVersion($version); + try + { + $this->uninstallPackageVersion($package, $version_entry->Version); + } + catch(Exception $e) + { + Console::outDebug(sprintf('warning: unable to uninstall package %s==%s, %s (%s)', $package, $version_entry->Version, $e->getMessage(), $e->getCode())); + } + } + } + + /** + * @param Package $package + * @param InstallationPaths $paths + * @throws InstallationException + */ + private static function initData(Package $package, InstallationPaths $paths): void + { + // Create data files + $dependencies = []; + foreach($package->Dependencies as $dependency) + { + $dependencies[] = $dependency->toArray(true); + } + + $data_files = [ + $paths->getDataPath() . DIRECTORY_SEPARATOR . 'assembly' => + ZiProto::encode($package->Assembly->toArray(true)), + $paths->getDataPath() . DIRECTORY_SEPARATOR . 'ext' => + ZiProto::encode($package->Header->CompilerExtension->toArray(true)), + $paths->getDataPath() . DIRECTORY_SEPARATOR . 'const' => + ZiProto::encode($package->Header->RuntimeConstants), + $paths->getDataPath() . DIRECTORY_SEPARATOR . 'dependencies' => + ZiProto::encode($dependencies), + ]; + + foreach($data_files as $file => $data) + { + try + { + IO::fwrite($file, $data); + } + catch (IOException $e) + { + throw new InstallationException('Cannot write to file \'' . $file . '\', ' . $e->getMessage(), $e); + } + } + } + + /** + * @return PackageLockManager|null + */ + private function getPackageLockManager(): ?PackageLockManager + { + if($this->PackageLockManager == null) + { + $this->PackageLockManager = new PackageLockManager(); + } + + return $this->PackageLockManager; + } + + } \ No newline at end of file diff --git a/src/ncc/Managers/ProjectManager.php b/src/ncc/Managers/ProjectManager.php index a9d0e42..286910b 100644 --- a/src/ncc/Managers/ProjectManager.php +++ b/src/ncc/Managers/ProjectManager.php @@ -1,12 +1,26 @@ SelectedDirectory = $selected_directory; $this->ProjectFilePath = null; $this->ProjectPath = null; - $this->detectProjectPath(); - } - - /** - * Attempts to resolve the project path from the selected directory - * Returns false if the selected directory is not a proper project or an initialized project - * - * @return void - */ - private function detectProjectPath(): void - { - $selected_directory = $this->SelectedDirectory; - // Auto-resolve the trailing slash /** @noinspection PhpStrFunctionsInspection */ - if(substr($selected_directory, -1) !== '/') + if(substr($path, -1) !== '/') { - $selected_directory .= DIRECTORY_SEPARATOR; + $path .= DIRECTORY_SEPARATOR; } // Detect if the folder exists or not - if(!file_exists($selected_directory) || !is_dir($selected_directory)) + if(!file_exists($path) || !is_dir($path)) { - return; + throw new DirectoryNotFoundException('The given directory \'' . $path .'\' does not exist'); } - $this->ProjectPath = $selected_directory; - $this->ProjectFilePath = $selected_directory . 'project.json'; + $this->ProjectPath = $path; + $this->ProjectFilePath = $path . 'project.json'; + + if(file_exists($this->ProjectFilePath)) + $this->load(); } /** * Initializes the project structure * - * // TODO: Correct the unexpected path behavior issue when initializing a project - * * @param Compiler $compiler * @param string $name * @param string $package - * @param string $src + * @param string|null $src * @param array $options * @throws InvalidPackageNameException * @throws InvalidProjectNameException * @throws MalformedJsonException * @throws ProjectAlreadyExistsException */ - public function initializeProject(Compiler $compiler, string $name, string $package, string $src, array $options=[]): void + public function initializeProject(Compiler $compiler, string $name, string $package, ?string $src=null, array $options=[]): void { // Validate the project information first if(!Validate::packageName($package)) @@ -109,41 +116,43 @@ throw new ProjectAlreadyExistsException('A project has already been initialized in \'' . $this->ProjectPath . DIRECTORY_SEPARATOR . 'project.json' . '\''); } - $Project = new ProjectConfiguration(); + $this->ProjectConfiguration = new ProjectConfiguration(); // Set the compiler information - $Project->Project->Compiler = $compiler; + $this->ProjectConfiguration->Project->Compiler = $compiler; // Set the assembly information - $Project->Assembly->Name = $name; - $Project->Assembly->Package = $package; - $Project->Assembly->Version = '1.0.0'; - $Project->Assembly->UUID = Uuid::v1()->toRfc4122(); + $this->ProjectConfiguration->Assembly->Name = $name; + $this->ProjectConfiguration->Assembly->Package = $package; + $this->ProjectConfiguration->Assembly->Version = '1.0.0'; + $this->ProjectConfiguration->Assembly->UUID = Uuid::v1()->toRfc4122(); // Set the build information - $Project->Build->SourcePath = $src; - $Project->Build->DefaultConfiguration = 'debug'; + $this->ProjectConfiguration->Build->SourcePath = $src; + if($this->ProjectConfiguration->Build->SourcePath == null) + $this->ProjectConfiguration->Build->SourcePath = $this->ProjectPath; + $this->ProjectConfiguration->Build->DefaultConfiguration = 'debug'; // Assembly constants if the program wishes to check for this - $Project->Build->DefineConstants['ASSEMBLY_NAME'] = '%ASSEMBLY.NAME%'; - $Project->Build->DefineConstants['ASSEMBLY_PACKAGE'] = '%ASSEMBLY.PACKAGE%'; - $Project->Build->DefineConstants['ASSEMBLY_VERSION'] = '%ASSEMBLY.VERSION%'; - $Project->Build->DefineConstants['ASSEMBLY_UID'] = '%ASSEMBLY.UID%'; + $this->ProjectConfiguration->Build->DefineConstants['ASSEMBLY_NAME'] = '%ASSEMBLY.NAME%'; + $this->ProjectConfiguration->Build->DefineConstants['ASSEMBLY_PACKAGE'] = '%ASSEMBLY.PACKAGE%'; + $this->ProjectConfiguration->Build->DefineConstants['ASSEMBLY_VERSION'] = '%ASSEMBLY.VERSION%'; + $this->ProjectConfiguration->Build->DefineConstants['ASSEMBLY_UID'] = '%ASSEMBLY.UID%'; // Generate configurations $DebugConfiguration = new ProjectConfiguration\BuildConfiguration(); $DebugConfiguration->Name = 'debug'; $DebugConfiguration->OutputPath = 'build/debug'; $DebugConfiguration->DefineConstants["DEBUG"] = '1'; // Debugging constant if the program wishes to check for this - $Project->Build->Configurations[] = $DebugConfiguration; + $this->ProjectConfiguration->Build->Configurations[] = $DebugConfiguration; $ReleaseConfiguration = new ProjectConfiguration\BuildConfiguration(); $ReleaseConfiguration->Name = 'release'; $ReleaseConfiguration->OutputPath = 'build/release'; $ReleaseConfiguration->DefineConstants["DEBUG"] = '0'; // Debugging constant if the program wishes to check for this - $Project->Build->Configurations[] = $ReleaseConfiguration; + $this->ProjectConfiguration->Build->Configurations[] = $ReleaseConfiguration; // Finally create project.json - $Project->toFile($this->ProjectPath . DIRECTORY_SEPARATOR . 'project.json'); + $this->ProjectConfiguration->toFile($this->ProjectPath . DIRECTORY_SEPARATOR . 'project.json'); // And create the project directory for additional assets/resources $Folders = [ @@ -166,15 +175,59 @@ switch($option) { case InitializeProjectOptions::CREATE_SOURCE_DIRECTORY: - if(!file_exists($this->ProjectPath . DIRECTORY_SEPARATOR . 'src')) + if(!file_exists($this->ProjectConfiguration->Build->SourcePath)) { - mkdir($this->ProjectPath . DIRECTORY_SEPARATOR . 'src'); + mkdir($this->ProjectConfiguration->Build->SourcePath); } break; } } } + /** + * Determines if a project configuration is loaded or not + * + * @return bool + */ + public function projectLoaded(): bool + { + if($this->ProjectConfiguration == null) + return false; + + return true; + } + + /** + * Attempts to load the project configuration + * + * @return void + * @throws MalformedJsonException + * @throws ProjectConfigurationNotFoundException + * @throws AccessDeniedException + * @throws FileNotFoundException + * @throws IOException + */ + public function load() + { + if(!file_exists($this->ProjectFilePath) && !is_file($this->ProjectFilePath)) + throw new ProjectConfigurationNotFoundException('The project configuration file \'' . $this->ProjectFilePath . '\' was not found'); + + $this->ProjectConfiguration = ProjectConfiguration::fromFile($this->ProjectFilePath); + } + + /** + * Saves the project configuration + * + * @return void + * @throws MalformedJsonException + */ + public function save() + { + if(!$this->projectLoaded()) + return; + $this->ProjectConfiguration->toFile($this->ProjectFilePath); + } + /** * @return string|null */ @@ -182,4 +235,49 @@ { return $this->ProjectFilePath; } + + /** + * @return ProjectConfiguration|null + * @throws AccessDeniedException + * @throws FileNotFoundException + * @throws IOException + * @throws MalformedJsonException + * @throws ProjectConfigurationNotFoundException + */ + public function getProjectConfiguration(): ?ProjectConfiguration + { + if($this->ProjectConfiguration == null) + $this->load(); + return $this->ProjectConfiguration; + } + + /** + * @return string|null + */ + public function getProjectPath(): ?string + { + return $this->ProjectPath; + } + + + /** + * Compiles the project into a package + * + * @param string $build_configuration + * @return string + * @throws AccessDeniedException + * @throws FileNotFoundException + * @throws IOException + * @throws MalformedJsonException + * @throws ProjectConfigurationNotFoundException + * @throws BuildConfigurationNotFoundException + * @throws BuildException + * @throws PackagePreparationFailedException + * @throws UnsupportedCompilerExtensionException + * @throws UnsupportedRunnerException + */ + public function build(string $build_configuration=BuildConfigurationValues::DefaultConfiguration): string + { + return PackageCompiler::compile($this, $build_configuration); + } } \ No newline at end of file diff --git a/src/ncc/Objects/ExecutionPointers.php b/src/ncc/Objects/ExecutionPointers.php new file mode 100644 index 0000000..9181c03 --- /dev/null +++ b/src/ncc/Objects/ExecutionPointers.php @@ -0,0 +1,171 @@ +Package = $package; + $this->Version = $version; + $this->Pointers = []; + } + + /** + * Adds an Execution Unit as a pointer + * + * @param ExecutionUnit $unit + * @param bool $overwrite + * @return bool + */ + public function addUnit(ExecutionUnit $unit, string $bin_file, bool $overwrite=true): bool + { + if(Validate::exceedsPathLength($bin_file)) + return false; + + if(!file_exists($bin_file)) + throw new FileNotFoundException('The file ' . $unit->Data . ' does not exist, cannot add unit \'' . $unit->ExecutionPolicy->Name . '\''); + + if($overwrite) + { + $this->deleteUnit($unit->ExecutionPolicy->Name); + } + elseif($this->getUnit($unit->ExecutionPolicy->Name) !== null) + { + return false; + } + + $this->Pointers[] = new ExecutionPointer($unit, $bin_file); + return true; + } + + /** + * Deletes an existing unit from execution pointers + * + * @param string $name + * @return bool + */ + public function deleteUnit(string $name): bool + { + $unit = $this->getUnit($name); + if($unit == null) + return false; + + $new_pointers = []; + foreach($this->Pointers as $pointer) + { + if($pointer->ExecutionPolicy->Name !== $name) + $new_pointers[] = $pointer; + } + + $this->Pointers = $new_pointers; + return true; + } + + /** + * Returns an existing unit from the pointers + * + * @param string $name + * @return ExecutionPointer|null + */ + public function getUnit(string $name): ?ExecutionPointer + { + foreach($this->Pointers as $pointer) + { + if($pointer->ExecutionPolicy->Name == $name) + return $pointer; + } + + return null; + } + + /** + * Returns an array of execution pointers that are currently configured + * + * @return array|ExecutionPointer[] + */ + public function getPointers(): array + { + return $this->Pointers; + } + + /** + * Returns the version of the package that uses these execution policies. + * + * @return string + */ + public function getVersion(): string + { + return $this->Version; + } + + /** + * Returns the name of the package that uses these execution policies + * + * @return string + */ + public function getPackage(): string + { + return $this->Package; + } + + /** + * Returns an array representation of the object + * + * @param bool $bytecode + * @return array + */ + public function toArray(bool $bytecode=false): array + { + $pointers = []; + foreach($this->Pointers as $pointer) + { + $pointers[] = $pointer->toArray($bytecode); + } + return $pointers; + } + + /** + * Constructs object from an array representation + * + * @param array $data + * @return ExecutionPointers + */ + public static function fromArray(array $data): self + { + $object = new self(); + + foreach($data as $datum) + { + $object->Pointers[] = ExecutionPointer::fromArray($datum); + } + + return $object; + } + } \ No newline at end of file diff --git a/src/ncc/Objects/ExecutionPointers/ExecutionPointer.php b/src/ncc/Objects/ExecutionPointers/ExecutionPointer.php new file mode 100644 index 0000000..9a5747e --- /dev/null +++ b/src/ncc/Objects/ExecutionPointers/ExecutionPointer.php @@ -0,0 +1,78 @@ +ID = $unit->getID(); + $this->ExecutionPolicy = $unit->ExecutionPolicy; + $this->FilePointer = $bin_file; + } + + /** + * Returns an array representation of the object + * + * @param bool $bytecode + * @return array + */ + public function toArray(bool $bytecode=false): array + { + return [ + ($bytecode ? Functions::cbc('id') : 'id') => $this->ID, + ($bytecode ? Functions::cbc('execution_policy') : 'execution_policy') => $this->ExecutionPolicy->toArray($bytecode), + ($bytecode ? Functions::cbc('file_pointer') : 'file_pointer') => $this->FilePointer, + ]; + } + + /** + * Constructs object from an array representation + * + * @param array $data + * @return ExecutionPointer + */ + public static function fromArray(array $data): self + { + $object = new self(); + + $object->ID = Functions::array_bc($data, 'id'); + $object->ExecutionPolicy = Functions::array_bc($data, 'execution_policy'); + $object->FilePointer = Functions::array_bc($data, 'file_pointer'); + + return $object; + } + } \ No newline at end of file diff --git a/src/ncc/Objects/InstallationPaths.php b/src/ncc/Objects/InstallationPaths.php new file mode 100644 index 0000000..d7c745a --- /dev/null +++ b/src/ncc/Objects/InstallationPaths.php @@ -0,0 +1,61 @@ +InstallationPath = $installation_path; + } + + /** + * Returns the data path where NCC's metadata & runtime information is stored + * + * @return string + */ + public function getDataPath(): string + { + return $this->InstallationPath . DIRECTORY_SEPARATOR . 'ncc'; + } + + /** + * Returns the source path for where the package resides + * + * @return string + */ + public function getSourcePath(): string + { + return $this->InstallationPath . DIRECTORY_SEPARATOR . 'src'; + } + + /** + * Returns the path for where executables are located + * + * @return string + */ + public function getBinPath(): string + { + return $this->InstallationPath . DIRECTORY_SEPARATOR . 'bin'; + } + + /** + * @return string + */ + public function getInstallationPath(): string + { + return $this->InstallationPath; + } + } \ No newline at end of file diff --git a/src/ncc/Objects/NccVersionInformation/Component.php b/src/ncc/Objects/NccVersionInformation/Component.php index 68b7568..33292ad 100644 --- a/src/ncc/Objects/NccVersionInformation/Component.php +++ b/src/ncc/Objects/NccVersionInformation/Component.php @@ -1,8 +1,14 @@ Vendor . DIRECTORY_SEPARATOR . $this->PackageName . DIRECTORY_SEPARATOR; - if(file_exists($component_path . 'VERSION') == false) - { + if(!file_exists($component_path . 'VERSION')) throw new ComponentVersionNotFoundException('The file \'' . $component_path . 'VERSION' . '\' does not exist'); - } - return file_get_contents($component_path . 'VERSION'); + return IO::fread($component_path . 'VERSION'); } /** diff --git a/src/ncc/Objects/Package.php b/src/ncc/Objects/Package.php index 78bf4dc..4dad3a6 100644 --- a/src/ncc/Objects/Package.php +++ b/src/ncc/Objects/Package.php @@ -4,17 +4,24 @@ namespace ncc\Objects; + use Exception; + use ncc\Abstracts\EncoderType; + use ncc\Abstracts\PackageStructureVersions; + use ncc\Exceptions\FileNotFoundException; use ncc\Exceptions\InvalidPackageException; use ncc\Exceptions\InvalidProjectConfigurationException; + use ncc\Exceptions\IOException; + use ncc\Exceptions\PackageParsingException; use ncc\Objects\Package\Component; + use ncc\Objects\Package\ExecutionUnit; use ncc\Objects\Package\Header; use ncc\Objects\Package\Installer; use ncc\Objects\Package\MagicBytes; - use ncc\Objects\Package\MainExecutionPolicy; use ncc\Objects\Package\Resource; use ncc\Objects\ProjectConfiguration\Assembly; use ncc\Objects\ProjectConfiguration\Dependency; use ncc\Utilities\Functions; + use ncc\Utilities\IO; use ncc\ZiProto\ZiProto; class Package @@ -50,7 +57,7 @@ /** * The Main Execution Policy object for the package if the package is an executable package. * - * @var MainExecutionPolicy|null + * @var string|null */ public $MainExecutionPolicy; @@ -61,6 +68,13 @@ */ public $Installer; + /** + * An array of execution units defined in the package + * + * @var ExecutionUnit[] + */ + public $ExecutionUnits; + /** * An array of resources that the package depends on * @@ -83,6 +97,7 @@ $this->MagicBytes = new MagicBytes(); $this->Header = new Header(); $this->Assembly = new Assembly(); + $this->ExecutionUnits = []; $this->Components = []; $this->Dependencies = []; $this->Resources = []; @@ -126,16 +141,141 @@ return true; } + /** + * Attempts to find the execution unit with the given name + * + * @param string $name + * @return ExecutionUnit|null + */ + public function getExecutionUnit(string $name): ?ExecutionUnit + { + foreach($this->ExecutionUnits as $unit) + { + if($unit->ExecutionPolicy->Name == $name) + return $unit; + } + + return null; + } + /** * Writes the package contents to disk * * @param string $output_path * @return void + * @throws IOException */ public function save(string $output_path): void { $package_contents = $this->MagicBytes->toString() . ZiProto::encode($this->toArray(true)); - file_put_contents($output_path, $package_contents); + IO::fwrite($output_path, $package_contents, 0777); + } + + /** + * Attempts to parse the specified package path and returns the object representation + * of the package, including with the MagicBytes representation that is in the + * file headers. + * + * @param string $path + * @return Package + * @throws FileNotFoundException + * @throws PackageParsingException + */ + public static function load(string $path): Package + { + if(!file_exists($path) || !is_file($path) || !is_readable($path)) + { + throw new FileNotFoundException('The file ' . $path . ' does not exist or is not readable'); + } + + $handle = fopen($path, "rb"); + $header = fread($handle, 256); // Read the first 256 bytes of the file + fclose($handle); + + if(!strtoupper(substr($header, 0, 11)) == 'NCC_PACKAGE') + throw new PackageParsingException('The package \'' . $path . '\' does not appear to be a valid NCC Package (Missing Header)'); + + // Extract the package structure version + $package_structure_version = strtoupper(substr($header, 11, 3)); + + if(!in_array($package_structure_version, PackageStructureVersions::ALL)) + throw new PackageParsingException('The package \'' . $path . '\' has a package structure version of ' . $package_structure_version . ' which is not supported by this version NCC'); + + // Extract the package encoding type and package type + $encoding_header = strtoupper(substr($header, 14, 5)); + $encoding_type = substr($encoding_header, 0, 3); + $package_type = substr($encoding_header, 3, 2); + + $magic_bytes = new MagicBytes(); + $magic_bytes->PackageStructureVersion = $package_structure_version; + + // Determine the encoding type + switch($encoding_type) + { + case '300': + $magic_bytes->Encoder = EncoderType::ZiProto; + $magic_bytes->IsCompressed = false; + $magic_bytes->IsEncrypted = false; + break; + + case '301': + $magic_bytes->Encoder = EncoderType::ZiProto; + $magic_bytes->IsCompressed = true; + $magic_bytes->IsEncrypted = false; + break; + + case '310': + $magic_bytes->Encoder = EncoderType::ZiProto; + $magic_bytes->IsCompressed = false; + $magic_bytes->IsEncrypted = true; + break; + + case '311': + $magic_bytes->Encoder = EncoderType::ZiProto; + $magic_bytes->IsCompressed = true; + $magic_bytes->IsEncrypted = true; + break; + + default: + throw new PackageParsingException('Cannot determine the encoding type for the package \'' . $path . '\' (Got ' . $encoding_type . ')'); + } + + // Determine the package type + switch($package_type) + { + case '40': + $magic_bytes->IsInstallable = true; + $magic_bytes->IsExecutable = false; + break; + + case '41': + $magic_bytes->IsInstallable = false; + $magic_bytes->IsExecutable = true; + break; + + case '42': + $magic_bytes->IsInstallable = true; + $magic_bytes->IsExecutable = true; + break; + + default: + throw new PackageParsingException('Cannot determine the package type for the package \'' . $path . '\' (Got ' . $package_type . ')'); + } + + // TODO: Implement encryption and compression parsing + + // Assuming all is good, load the entire fire into memory and parse its contents + try + { + $package = Package::fromArray(ZiProto::decode(substr(IO::fread($path), strlen($magic_bytes->toString())))); + } + catch(Exception $e) + { + throw new PackageParsingException('Cannot decode the contents of the package \'' . $path . '\', invalid encoding or the package is corrupted, ' . $e->getMessage(), $e); + } + + $package->MagicBytes = $magic_bytes; + return $package; } /** @@ -161,13 +301,17 @@ foreach($this->Resources as $resource) $_resources[] = $resource->toArray($bytecode); + $_execution_units = []; + foreach($this->ExecutionUnits as $unit) + $_execution_units[] = $unit->toArray($bytecode); return [ - ($bytecode ? Functions::cbc('header') : 'header') => $this->Header?->toArray($bytecode), - ($bytecode ? Functions::cbc('assembly') : 'assembly') => $this->Assembly?->toArray($bytecode), + ($bytecode ? Functions::cbc('header') : 'header') => $this?->Header?->toArray($bytecode), + ($bytecode ? Functions::cbc('assembly') : 'assembly') => $this?->Assembly?->toArray($bytecode), ($bytecode ? Functions::cbc('dependencies') : 'dependencies') => $_dependencies, - ($bytecode ? Functions::cbc('main_execution_policy') : 'main_execution_policy') => $this->MainExecutionPolicy?->toArray($bytecode), - ($bytecode ? Functions::cbc('installer') : 'installer') => $this->Installer?->toArray($bytecode), + ($bytecode ? Functions::cbc('main_execution_policy') : 'main_execution_policy') => $this?->MainExecutionPolicy, + ($bytecode ? Functions::cbc('installer') : 'installer') => $this?->Installer?->toArray($bytecode), + ($bytecode ? Functions::cbc('execution_units') : 'execution_units') => $_execution_units, ($bytecode ? Functions::cbc('resources') : 'resources') => $_resources, ($bytecode ? Functions::cbc('components') : 'components') => $_components ]; @@ -190,8 +334,6 @@ $object->Assembly = Assembly::fromArray($object->Assembly); $object->MainExecutionPolicy = Functions::array_bc($data, 'main_execution_policy'); - if($object->MainExecutionPolicy !== null) - $object->MainExecutionPolicy = MainExecutionPolicy::fromArray($object->MainExecutionPolicy); $object->Installer = Functions::array_bc($data, 'installer'); if($object->Installer !== null) @@ -224,6 +366,15 @@ } } + $_execution_units = Functions::array_bc($data, 'execution_units'); + if($_execution_units !== null) + { + foreach($_execution_units as $unit) + { + $object->ExecutionUnits[] = ExecutionUnit::fromArray($unit); + } + } + return $object; } } \ No newline at end of file diff --git a/src/ncc/Objects/Package/Component.php b/src/ncc/Objects/Package/Component.php index 75406f7..b48708b 100644 --- a/src/ncc/Objects/Package/Component.php +++ b/src/ncc/Objects/Package/Component.php @@ -1,5 +1,7 @@ Checksum === null) - return false; + return true; // Return true if the checksum is empty if($this->Data === null) - return false; + return true; // Return true if the data is null - if(hash('sha1', $this->Data) !== $this->Checksum) - return false; + if(hash('sha1', $this->Data, true) !== $this->Checksum) + return false; // Return false if the checksum failed return true; } + /** + * Updates the checksum of the resource + * + * @return void + */ + public function updateChecksum(): void + { + $this->Checksum = null; + + if(gettype($this->Data) == 'string') + { + $this->Checksum = hash('sha1', $this->Data, true); + } + } + /** * Returns an array representation of the component. * diff --git a/src/ncc/Objects/Package/ExecutionUnit.php b/src/ncc/Objects/Package/ExecutionUnit.php new file mode 100644 index 0000000..75940cc --- /dev/null +++ b/src/ncc/Objects/Package/ExecutionUnit.php @@ -0,0 +1,71 @@ + $this->ExecutionPolicy->toArray($bytecode), + ($bytecode ? Functions::cbc('data') : 'data') => $this->Data, + ]; + } + + /** + * Constructs object from an array representation + * + * @param array $data + * @return static + */ + public static function fromArray(array $data): self + { + $object = new self(); + + $object->ExecutionPolicy = Functions::array_bc($data, 'execution_policy'); + $object->Data = Functions::array_bc($data, 'data'); + + return $object; + } + + /** + * @return string + */ + public function getID(): string + { + if($this->ID == null) + $this->ID = hash('sha1', $this->ExecutionPolicy->Name); + return $this->ID; + } + + } \ No newline at end of file diff --git a/src/ncc/Objects/Package/Header.php b/src/ncc/Objects/Package/Header.php index e090bc5..16a627e 100644 --- a/src/ncc/Objects/Package/Header.php +++ b/src/ncc/Objects/Package/Header.php @@ -68,6 +68,9 @@ $object->RuntimeConstants = Functions::array_bc($data, 'runtime_constants'); $object->CompilerVersion = Functions::array_bc($data, 'compiler_version'); + if($object->CompilerExtension !== null) + $object->CompilerExtension = Compiler::fromArray($object->CompilerExtension); + return $object; } } \ No newline at end of file diff --git a/src/ncc/Objects/Package/Installer.php b/src/ncc/Objects/Package/Installer.php index cbeb8b0..c368aac 100644 --- a/src/ncc/Objects/Package/Installer.php +++ b/src/ncc/Objects/Package/Installer.php @@ -1,18 +1,81 @@ PreInstall == null && $this->PostInstall == null && + $this->PreUninstall == null && $this->PostUninstall == null && + $this->PreUpdate == null && $this->PostUpdate == null + ) + { + return null; + } + + return [ + ($bytecode ? Functions::cbc('pre_install') : 'pre_install') => ($this->PreInstall == null ? null : $this->PreInstall), + ($bytecode ? Functions::cbc('post_install') : 'post_install') => ($this->PostInstall == null ? null : $this->PostInstall), + ($bytecode ? Functions::cbc('pre_uninstall') : 'pre_uninstall') => ($this->PreUninstall == null ? null : $this->PreUninstall), + ($bytecode? Functions::cbc('post_uninstall') : 'post_uninstall') => ($this->PostUninstall == null ? null : $this->PostUninstall), + ($bytecode? Functions::cbc('pre_update') : 'pre_update') => ($this->PreUpdate == null ? null : $this->PreUpdate), + ($bytecode? Functions::cbc('post_update') : 'post_update') => ($this->PostUpdate == null ? null : $this->PostUpdate) + ]; } /** @@ -20,11 +83,19 @@ * * @param array $data * @return Installer + * @noinspection DuplicatedCode */ public static function fromArray(array $data): self { $object = new self(); + $object->PreInstall = Functions::array_bc($data, 'pre_install'); + $object->PostInstall = Functions::array_bc($data, 'post_install'); + $object->PreUninstall = Functions::array_bc($data, 'pre_uninstall'); + $object->PostUninstall = Functions::array_bc($data, 'post_uninstall'); + $object->PreUpdate = Functions::array_bc($data, 'pre_update'); + $object->PostUpdate = Functions::array_bc($data, 'post_update'); + return $object; } } \ No newline at end of file diff --git a/src/ncc/Objects/Package/MagicBytes.php b/src/ncc/Objects/Package/MagicBytes.php index df54f71..3e86446 100644 --- a/src/ncc/Objects/Package/MagicBytes.php +++ b/src/ncc/Objects/Package/MagicBytes.php @@ -147,6 +147,12 @@ // NCC_PACKAGE1.030140 $magic_bytes .= '40'; } + else + { + // If no type is specified, default to installable only + // NCC_PACKAGE1.030140 + $magic_bytes .= '40'; + } return $magic_bytes; } diff --git a/src/ncc/Objects/Package/MainExecutionPolicy.php b/src/ncc/Objects/Package/MainExecutionPolicy.php deleted file mode 100644 index c005578..0000000 --- a/src/ncc/Objects/Package/MainExecutionPolicy.php +++ /dev/null @@ -1,30 +0,0 @@ -Data === null) return false; - if(hash('sha1', $this->Data) !== $this->Checksum) + if(hash('sha1', $this->Data, true) !== $this->Checksum) return false; return true; } + /** + * Updates the checksum of the resource + * + * @return void + */ + public function updateChecksum(): void + { + $this->Checksum = null; + + if(gettype($this->Data) == 'string') + { + $this->Checksum = hash('sha1', $this->Data, true); + } + } + /** * Returns an array representation of the resource. * diff --git a/src/ncc/Objects/PackageLock.php b/src/ncc/Objects/PackageLock.php new file mode 100644 index 0000000..3e4d055 --- /dev/null +++ b/src/ncc/Objects/PackageLock.php @@ -0,0 +1,194 @@ +PackageLockVersion = Versions::PackageLockVersion; + $this->Packages = []; + } + + /** + * Updates the version and timestamp + * + * @return void + */ + private function update(): void + { + $this->PackageLockVersion = Versions::PackageLockVersion; + $this->LastUpdatedTimestamp = time(); + } + + /** + * @param Package $package + * @param string $install_path + * @return void + */ + public function addPackage(Package $package, string $install_path): void + { + if(!isset($this->Packages[$package->Assembly->Package])) + { + $package_entry = new PackageEntry(); + $package_entry->addVersion($package, $install_path, true); + $package_entry->Name = $package->Assembly->Package; + $this->Packages[$package->Assembly->Package] = $package_entry; + $this->update(); + + return; + } + + $this->Packages[$package->Assembly->Package]->addVersion($package, true); + $this->update(); + } + + /** + * Removes a package version entry, removes the entire entry if there are no installed versions + * + * @param string $package + * @param string $version + * @return bool + */ + public function removePackageVersion(string $package, string $version): bool + { + if(isset($this->Packages[$package])) + { + $r = $this->Packages[$package]->removeVersion($version); + + // Remove the entire package entry if there's no installed versions + if($this->Packages[$package]->getLatestVersion() == null && $r) + { + unset($this->Packages[$package]); + } + + $this->update(); + return $r; + } + + return false; + } + + /** + * Removes an entire package entry + * + * @param string $package + * @return bool + */ + public function removePackage(string $package): bool + { + if(isset($this->Packages[$package])) + { + unset($this->Packages[$package]); + return true; + } + + return false; + } + + /** + * Gets an existing package entry, returns null if no such entry exists + * + * @param string $package + * @return PackageEntry|null + */ + public function getPackage(string $package): ?PackageEntry + { + if(isset($this->Packages[$package])) + { + return $this->Packages[$package]; + } + + return null; + } + + /** + * Returns an array of all packages and their installed versions + * + * @return array + */ + public function getPackages(): array + { + $results = []; + foreach($this->Packages as $package => $entry) + $results[$package] = $entry->getVersions(); + return $results; + } + + /** + * Returns an array representation of the object + * + * @param bool $bytecode + * @return array + */ + public function toArray(bool $bytecode=false): array + { + $package_entries = []; + foreach($this->Packages as $entry) + { + $package_entries[] = $entry->toArray($bytecode); + } + + return [ + ($bytecode ? Functions::cbc('package_lock_version') : 'package_lock_version') => $this->PackageLockVersion, + ($bytecode ? Functions::cbc('last_updated_timestamp') : 'last_updated_timestamp') => $this->LastUpdatedTimestamp, + ($bytecode ? Functions::cbc('packages') : 'packages') => $package_entries + ]; + } + + /** + * Constructs object from an array representation + * + * @param array $data + * @return static + */ + public static function fromArray(array $data): self + { + $object = new self(); + + $packages = Functions::array_bc($data, 'packages'); + if($packages !== null) + { + foreach($packages as $_datum) + { + $entry = PackageEntry::fromArray($_datum); + $object->Packages[$entry->Name] = $entry; + } + } + + $object->PackageLockVersion = Functions::array_bc($data, 'package_lock_version'); + $object->LastUpdatedTimestamp = Functions::array_bc($data, 'last_updated_timestamp'); + + return $object; + } + } \ No newline at end of file diff --git a/src/ncc/Objects/PackageLock/DependencyEntry.php b/src/ncc/Objects/PackageLock/DependencyEntry.php new file mode 100644 index 0000000..82be14e --- /dev/null +++ b/src/ncc/Objects/PackageLock/DependencyEntry.php @@ -0,0 +1,64 @@ +PackageName = $dependency->Name; + $this->Version = $dependency->Version; + } + } + + /** + * Returns an array representation of the object + * + * @param bool $bytecode + * @return array + */ + public function toArray(bool $bytecode=false): array + { + return [ + ($bytecode ? Functions::cbc('package_name') : 'package_name') => $this->PackageName, + ($bytecode ? Functions::cbc('version') : 'version') => $this->Version, + ]; + } + + /** + * Constructs object from an array representation + * + * @param array $data + * @return DependencyEntry + */ + public static function fromArray(array $data): self + { + $object = new self(); + + $object->PackageName = Functions::array_bc($data, 'package_name'); + $object->Version = Functions::array_bc($data, 'version'); + + return $object; + } + } \ No newline at end of file diff --git a/src/ncc/Objects/PackageLock/PackageEntry.php b/src/ncc/Objects/PackageLock/PackageEntry.php new file mode 100644 index 0000000..43cf7a9 --- /dev/null +++ b/src/ncc/Objects/PackageLock/PackageEntry.php @@ -0,0 +1,235 @@ +Versions = []; + } + + /** + * Searches and returns a version of the package + * + * @param string $version + * @param bool $throw_exception + * @return VersionEntry|null + * @throws VersionNotFoundException + */ + public function getVersion(string $version, bool $throw_exception=false): ?VersionEntry + { + foreach($this->Versions as $versionEntry) + { + if($versionEntry->Version == $version) + { + return $versionEntry; + } + } + + if($throw_exception) + throw new VersionNotFoundException('The version entry is not found'); + + return null; + } + + /** + * Removes version entry from the package + * + * @param string $version + * @return bool + * @noinspection PhpUnused + */ + public function removeVersion(string $version): bool + { + $count = 0; + $found_node = false; + foreach($this->Versions as $versionEntry) + { + if($versionEntry->Version == $version) + { + $found_node = true; + break; + } + + $count += 1; + } + + if($found_node) + { + unset($this->Versions[$count]); + $this->updateLatestVersion(); + return true; + } + + return false; + } + + /** + * Adds a new version entry to the package, if overwrite is true then + * the entry will be overwritten if it exists, otherwise it will return + * false. + * + * @param Package $package + * @param string $install_path + * @param bool $overwrite + * @return bool + */ + public function addVersion(Package $package, string $install_path, bool $overwrite=false): bool + { + try + { + if ($this->getVersion($package->Assembly->Version) !== null) + { + if (!$overwrite) return false; + $this->removeVersion($package->Assembly->Version); + } + } + catch (VersionNotFoundException $e) + { + unset($e); + } + + $version = new VersionEntry(); + $version->Version = $package->Assembly->Version; + $version->Compiler = $package->Header->CompilerExtension; + $version->ExecutionUnits = $package->ExecutionUnits; + $version->MainExecutionPolicy = $package->MainExecutionPolicy; + $version->Location = $install_path; + + foreach($package->Dependencies as $dependency) + { + $version->Dependencies[] = new DependencyEntry($dependency); + } + + $this->Versions[] = $version; + $this->updateLatestVersion(); + return true; + } + + /** + * Updates and returns the latest version of this package entry + * + * @return void + */ + private function updateLatestVersion(): void + { + $latest_version = null; + foreach($this->Versions as $version) + { + $version = $version->Version; + if($latest_version == null) + { + $latest_version = $version; + continue; + } + if(VersionComparator::compareVersion($version, $latest_version)) + $latest_version = $version; + } + + $this->LatestVersion = $latest_version; + } + + /** + * @return string|null + */ + public function getLatestVersion(): ?string + { + return $this->LatestVersion; + } + + /** + * Returns an array of all versions installed + * + * @return array + */ + public function getVersions(): array + { + $r = []; + + foreach($this->Versions as $version) + { + $r[] = $version->Version; + } + + return $r; + } + + /** + * Returns an array representation of the object + * + * @param bool $bytecode + * @return array + */ + public function toArray(bool $bytecode=false): array + { + $versions = []; + foreach($this->Versions as $version) + { + $versions[] = $version->toArray($bytecode); + } + + return [ + ($bytecode ? Functions::cbc('name') : 'name') => $this->Name, + ($bytecode ? Functions::cbc('latest_version') : 'latest_version') => $this->LatestVersion, + ($bytecode ? Functions::cbc('versions') : 'versions') => $versions, + ]; + } + + /** + * Constructs object from an array representation + * + * @param array $data + * @return PackageEntry + */ + public static function fromArray(array $data): self + { + $object = new self(); + + $object->Name = Functions::array_bc($data, 'name'); + $object->LatestVersion = Functions::array_bc($data, 'latest_version'); + $versions = Functions::array_bc($data, 'versions'); + + if($versions !== null) + { + foreach($versions as $_datum) + { + $object->Versions[] = VersionEntry::fromArray($_datum); + } + } + + return $object; + } + + } \ No newline at end of file diff --git a/src/ncc/Objects/PackageLock/VersionEntry.php b/src/ncc/Objects/PackageLock/VersionEntry.php new file mode 100644 index 0000000..f512c4a --- /dev/null +++ b/src/ncc/Objects/PackageLock/VersionEntry.php @@ -0,0 +1,126 @@ +Dependencies = []; + $this->ExecutionUnits = []; + } + + /** + * Returns an array representation of the object + * + * @param bool $bytecode + * @return array + */ + public function toArray(bool $bytecode=false): array + { + $dependencies = []; + foreach($this->Dependencies as $dependency) + { + $dependencies[] = $dependency->toArray($bytecode); + } + + $execution_units = []; + foreach($this->ExecutionUnits as $executionUnit) + { + $execution_units[] = $executionUnit->toArray($bytecode); + } + + return [ + ($bytecode ? Functions::cbc('version') : 'version') => $this->Version, + ($bytecode ? Functions::cbc('compiler') : 'compiler') => $this->Compiler->toArray($bytecode), + ($bytecode ? Functions::cbc('dependencies') : 'dependencies') => $dependencies, + ($bytecode ? Functions::cbc('execution_units') : 'execution_units') => $execution_units, + ($bytecode ? Functions::cbc('main_execution_policy') : 'main_execution_policy') => $this->MainExecutionPolicy, + ($bytecode ? Functions::cbc('location') : 'location') => $this->Location, + ]; + } + + /** + * Constructs object from an array representation + * + * @param array $data + * @return VersionEntry + */ + public static function fromArray(array $data): self + { + $object = new self(); + $object->Version = Functions::array_bc($data, 'version'); + $object->Compiler = Compiler::fromArray(Functions::array_bc($data, 'compiler')); + $object->MainExecutionPolicy = Functions::array_bc($data, 'main_execution_policy'); + $object->Location = Functions::array_bc($data, 'location'); + + $dependencies = Functions::array_bc($data, 'dependencies'); + if($dependencies !== null) + { + foreach($dependencies as $_datum) + { + $object->Dependencies[] = DependencyEntry::fromArray($_datum); + } + } + + $execution_units = Functions::array_bc($data, 'execution_units'); + if($execution_units !== null) + { + foreach($execution_units as $_datum) + { + $object->ExecutionUnits[] = ExecutionUnit::fromArray($_datum); + } + } + + return $object; + } + } \ No newline at end of file diff --git a/src/ncc/Objects/ProjectConfiguration.php b/src/ncc/Objects/ProjectConfiguration.php index f2980b1..f361a3e 100644 --- a/src/ncc/Objects/ProjectConfiguration.php +++ b/src/ncc/Objects/ProjectConfiguration.php @@ -1,18 +1,29 @@ Project = new Project(); $this->Assembly = new Assembly(); + $this->ExecutionPolicies = []; $this->Build = new Build(); } @@ -58,13 +84,15 @@ * * @param bool $throw_exception * @return bool + * @throws BuildConfigurationNotFoundException + * @throws InvalidConstantNameException + * @throws InvalidProjectBuildConfiguration * @throws InvalidProjectConfigurationException * @throws InvalidPropertyValueException * @throws RuntimeException + * @throws UndefinedExecutionPolicyException * @throws UnsupportedCompilerExtensionException * @throws UnsupportedExtensionVersionException - * @throws InvalidProjectBuildConfiguration - * @throws InvalidConstantNameException */ public function validate(bool $throw_exception=True): bool { @@ -77,9 +105,170 @@ if(!$this->Build->validate($throw_exception)) return false; + try + { + $this->getRequiredExecutionPolicies(BuildConfigurationValues::AllConfigurations); + } + catch(Exception $e) + { + if($throw_exception) + throw $e; + return false; + } + return true; } + /** + * @param string $name + * @return ExecutionPolicy|null + */ + private function getExecutionPolicy(string $name): ?ExecutionPolicy + { + foreach($this->ExecutionPolicies as $executionPolicy) + { + if($executionPolicy->Name == $name) + return $executionPolicy; + } + + return null; + } + + /** + * Runs a check on the project configuration and determines what policies are required + * + * @param string $build_configuration + * @return array + * @throws BuildConfigurationNotFoundException + * @throws UndefinedExecutionPolicyException + */ + public function getRequiredExecutionPolicies(string $build_configuration=BuildConfigurationValues::DefaultConfiguration): array + { + if($this->ExecutionPolicies == null || count($this->ExecutionPolicies) == 0) + return []; + + $defined_polices = []; + $required_policies = []; + /** @var ExecutionPolicy $execution_policy */ + foreach($this->ExecutionPolicies as $execution_policy) + { + $defined_polices[] = $execution_policy->Name; + $execution_policy->validate(); + } + + // Check the installer by batch + if($this->Installer !== null) + { + $array_rep = $this->Installer->toArray(); + /** @var string[] $value */ + foreach($array_rep as $key => $value) + { + if($value == null || count($value) == 0) + continue; + + foreach($value as $unit) + { + if(!in_array($unit, $defined_polices)) + throw new UndefinedExecutionPolicyException('The property \'' . $key . '\' in the project configuration calls for an undefined execution policy \'' . $unit . '\''); + if(!in_array($unit, $required_policies)) + $required_policies[] = $unit; + } + } + } + + if($this->Build->PreBuild !== null && count($this->Build->PostBuild) > 0) + { + foreach($this->Build->PostBuild as $unit) + { + if(!in_array($unit, $defined_polices)) + throw new UndefinedExecutionPolicyException('The property \'build.pre_build\' in the project configuration calls for an undefined execution policy \'' . $unit . '\''); + if(!in_array($unit, $required_policies)) + $required_policies[] = $unit; + } + } + + if($this->Build->PostBuild !== null && count($this->Build->PostBuild) > 0) + { + foreach($this->Build->PostBuild as $unit) + { + if(!in_array($unit, $defined_polices)) + throw new UndefinedExecutionPolicyException('The property \'build.pre_build\' in the project configuration calls for an undefined execution policy \'' . $unit . '\''); + if(!in_array($unit, $required_policies)) + $required_policies[] = $unit; + } + } + + switch($build_configuration) + { + case BuildConfigurationValues::AllConfigurations: + /** @var BuildConfiguration $configuration */ + foreach($this->Build->Configurations as $configuration) + { + foreach($this->processBuildPolicies($configuration, $defined_polices) as $policy) + { + if(!in_array($policy, $required_policies)) + $required_policies[] = $policy; + } + } + break; + + default: + $configuration = $this->Build->getBuildConfiguration($build_configuration); + foreach($this->processBuildPolicies($configuration, $defined_polices) as $policy) + { + if(!in_array($policy, $required_policies)) + $required_policies[] = $policy; + } + break; + } + + foreach($required_policies as $policy) + { + $execution_policy = $this->getExecutionPolicy($policy); + if($execution_policy->ExitHandlers !== null) + { + if( + $execution_policy->ExitHandlers->Success !== null && + $execution_policy->ExitHandlers->Success->Run !== null + ) + { + if(!in_array($execution_policy->ExitHandlers->Success->Run, $defined_polices)) + throw new UndefinedExecutionPolicyException('The execution policy \'' . $execution_policy->Name . '\' Success exit handler points to a undefined execution policy \'' . $execution_policy->ExitHandlers->Success->Run . '\''); + + if(!in_array($execution_policy->ExitHandlers->Success->Run, $required_policies)) + $required_policies[] = $execution_policy->ExitHandlers->Success->Run; + } + + if( + $execution_policy->ExitHandlers->Warning !== null && + $execution_policy->ExitHandlers->Warning->Run !== null + ) + { + if(!in_array($execution_policy->ExitHandlers->Warning->Run, $defined_polices)) + throw new UndefinedExecutionPolicyException('The execution policy \'' . $execution_policy->Name . '\' Warning exit handler points to a undefined execution policy \'' . $execution_policy->ExitHandlers->Warning->Run . '\''); + + if(!in_array($execution_policy->ExitHandlers->Warning->Run, $required_policies)) + $required_policies[] = $execution_policy->ExitHandlers->Warning->Run; + } + + if( + $execution_policy->ExitHandlers->Error !== null && + $execution_policy->ExitHandlers->Error->Run !== null + ) + { + if(!in_array($execution_policy->ExitHandlers->Error->Run, $defined_polices)) + throw new UndefinedExecutionPolicyException('The execution policy \'' . $execution_policy->Name . '\' Error exit handler points to a undefined execution policy \'' . $execution_policy->ExitHandlers->Error->Run . '\''); + + if(!in_array($execution_policy->ExitHandlers->Error->Run, $required_policies)) + $required_policies[] = $execution_policy->ExitHandlers->Error->Run; + } + } + + } + + return $required_policies; + } + /** * Returns an array representation of the object * @@ -88,9 +277,15 @@ */ public function toArray(bool $bytecode=false): array { + $execution_policies = []; + foreach($this->ExecutionPolicies as $executionPolicy) + { + $execution_policies[$executionPolicy->Name] = $executionPolicy->toArray($bytecode); + } return [ ($bytecode ? Functions::cbc('project') : 'project') => $this->Project->toArray($bytecode), ($bytecode ? Functions::cbc('assembly') : 'assembly') => $this->Assembly->toArray($bytecode), + ($bytecode ? Functions::cbc('execution_policies') : 'execution_policies') => $execution_policies, ($bytecode ? Functions::cbc('build') : 'build') => $this->Build->toArray($bytecode), ]; } @@ -128,7 +323,26 @@ $ProjectConfigurationObject->Project = Project::fromArray(Functions::array_bc($data, 'project')); $ProjectConfigurationObject->Assembly = Assembly::fromArray(Functions::array_bc($data, 'assembly')); + $ProjectConfigurationObject->ExecutionPolicies = Functions::array_bc($data, 'execution_policies'); $ProjectConfigurationObject->Build = Build::fromArray(Functions::array_bc($data, 'build')); + $ProjectConfigurationObject->Installer = Functions::array_bc($data, 'installer'); + + if($ProjectConfigurationObject->Installer !== null) + $ProjectConfigurationObject->Installer = Installer::fromArray($ProjectConfigurationObject->Installer); + + if($ProjectConfigurationObject->ExecutionPolicies == null) + { + $ProjectConfigurationObject->ExecutionPolicies = []; + } + else + { + $policies = []; + foreach($ProjectConfigurationObject->ExecutionPolicies as $policy) + { + $policies[] = ExecutionPolicy::fromArray($policy); + } + $ProjectConfigurationObject->ExecutionPolicies = $policies; + } return $ProjectConfigurationObject; } @@ -140,10 +354,45 @@ * @return ProjectConfiguration * @throws FileNotFoundException * @throws MalformedJsonException + * @throws AccessDeniedException + * @throws IOException * @noinspection PhpUnused */ public static function fromFile(string $path): ProjectConfiguration { return ProjectConfiguration::fromArray(Functions::loadJsonFile($path, Functions::FORCE_ARRAY)); } + + /** + * @param BuildConfiguration $configuration + * @param array $defined_polices + * @return array + * @throws UndefinedExecutionPolicyException + */ + private function processBuildPolicies(BuildConfiguration $configuration, array $defined_polices): array + { + $required_policies = []; + + if ($configuration->PreBuild !== null && count($configuration->PreBuild) > 0) + { + foreach ($configuration->PreBuild as $unit) + { + if (!in_array($unit, $defined_polices)) + throw new UndefinedExecutionPolicyException('The property \'pre_build\' in the build configuration \'' . $configuration->Name . '\' calls for an undefined execution policy \'' . $unit . '\''); + $required_policies[] = $unit; + } + } + + if ($configuration->PostBuild !== null && count($configuration->PostBuild) > 0) + { + foreach ($configuration->PostBuild as $unit) + { + if (!in_array($unit, $defined_polices)) + throw new UndefinedExecutionPolicyException('The property \'pre_build\' in the build configuration \'' . $configuration->Name . '\' calls for an undefined execution policy \'' . $unit . '\''); + $required_policies[] = $unit; + } + } + + return $required_policies; + } } \ No newline at end of file diff --git a/src/ncc/Objects/ProjectConfiguration/Build.php b/src/ncc/Objects/ProjectConfiguration/Build.php index cf45987..71caa85 100644 --- a/src/ncc/Objects/ProjectConfiguration/Build.php +++ b/src/ncc/Objects/ProjectConfiguration/Build.php @@ -4,6 +4,7 @@ namespace ncc\Objects\ProjectConfiguration; + use ncc\Abstracts\Options\BuildConfigurationValues; use ncc\Exceptions\BuildConfigurationNotFoundException; use ncc\Exceptions\InvalidConstantNameException; use ncc\Exceptions\InvalidProjectBuildConfiguration; @@ -51,6 +52,13 @@ */ public $Scope; + /** + * The execution policy to use as the main execution point + * + * @var string|null + */ + public $Main; + /** * An array of constants to define by default * @@ -58,6 +66,20 @@ */ public $DefineConstants; + /** + * An array of execution policies to execute pre build + * + * @var string[] + */ + public $PreBuild; + + /** + * An array of execution policies to execute post build + * + * @var string[] + */ + public $PostBuild; + /** * An array of dependencies that are required by default * @@ -125,6 +147,7 @@ * Returns an array of all the build configurations defined in the project configuration * * @return array + * @noinspection PhpUnused */ public function getBuildConfigurations(): array { @@ -148,6 +171,9 @@ */ public function getBuildConfiguration(string $name): BuildConfiguration { + if($name == BuildConfigurationValues::DefaultConfiguration) + $name = $this->DefaultConfiguration; + foreach($this->Configurations as $configuration) { if($configuration->Name == $name) @@ -174,7 +200,10 @@ $ReturnResults[($bytecode ? Functions::cbc('exclude_files') : 'exclude_files')] = $this->ExcludeFiles; $ReturnResults[($bytecode ? Functions::cbc('options') : 'options')] = $this->Options; $ReturnResults[($bytecode ? Functions::cbc('scope') : 'scope')] = $this->Scope; + $ReturnResults[($bytecode ? Functions::cbc('main') : 'main')] = $this->Main; $ReturnResults[($bytecode ? Functions::cbc('define_constants') : 'define_constants')] = $this->DefineConstants; + $ReturnResults[($bytecode ? Functions::cbc('pre_build') : 'pre_build')] = $this->PreBuild; + $ReturnResults[($bytecode ? Functions::cbc('post_build') : 'post_build')] = $this->PostBuild; $ReturnResults[($bytecode ? Functions::cbc('dependencies') : 'dependencies')] = []; foreach($this->Dependencies as $dependency) @@ -207,7 +236,10 @@ $BuildObject->ExcludeFiles = (Functions::array_bc($data, 'exclude_files') ?? []); $BuildObject->Options = (Functions::array_bc($data, 'options') ?? []); $BuildObject->Scope = Functions::array_bc($data, 'scope'); + $BuildObject->Main = Functions::array_bc($data, 'main'); $BuildObject->DefineConstants = (Functions::array_bc($data, 'define_constants') ?? []); + $BuildObject->PreBuild = (Functions::array_bc($data, 'pre_build') ?? []); + $BuildObject->PostBuild = (Functions::array_bc($data, 'post_build') ?? []); if(Functions::array_bc($data, 'dependencies') !== null) { diff --git a/src/ncc/Objects/ProjectConfiguration/BuildConfiguration.php b/src/ncc/Objects/ProjectConfiguration/BuildConfiguration.php index cc80ef6..45a8bf5 100644 --- a/src/ncc/Objects/ProjectConfiguration/BuildConfiguration.php +++ b/src/ncc/Objects/ProjectConfiguration/BuildConfiguration.php @@ -47,6 +47,20 @@ */ public $ExcludeFiles; + /** + * An array of policies to execute pre-building the package + * + * @var string[]|string + */ + public $PreBuild; + + /** + * An array of policies to execute post-building the package + * + * @var string + */ + public $PostBuild; + /** * Dependencies required for the build configuration, cannot conflict with the * default dependencies @@ -64,6 +78,8 @@ $this->OutputPath = 'build'; $this->DefineConstants = []; $this->ExcludeFiles = []; + $this->PreBuild = []; + $this->PostBuild = []; $this->Dependencies = []; } @@ -84,6 +100,7 @@ $ReturnResults[($bytecode ? Functions::cbc('output_path') : 'output_path')] = $this->OutputPath; $ReturnResults[($bytecode ? Functions::cbc('define_constants') : 'define_constants')] = $this->DefineConstants; $ReturnResults[($bytecode ? Functions::cbc('exclude_files') : 'exclude_files')] = $this->ExcludeFiles; + $ReturnResults[($bytecode ? Functions::cbc('pre_build') : 'pre_build')] = $this->PreBuild; $ReturnResults[($bytecode ? Functions::cbc('dependencies') : 'dependencies')] = []; foreach($this->Dependencies as $dependency) @@ -105,36 +122,30 @@ $BuildConfigurationObject = new BuildConfiguration(); if(Functions::array_bc($data, 'name') !== null) - { $BuildConfigurationObject->Name = Functions::array_bc($data, 'name'); - } if(Functions::array_bc($data, 'options') !== null) - { $BuildConfigurationObject->Options = Functions::array_bc($data, 'options'); - } if(Functions::array_bc($data, 'output_path') !== null) - { $BuildConfigurationObject->OutputPath = Functions::array_bc($data, 'output_path'); - } if(Functions::array_bc($data, 'define_constants') !== null) - { $BuildConfigurationObject->DefineConstants = Functions::array_bc($data, 'define_constants'); - } if(Functions::array_bc($data, 'exclude_files') !== null) - { $BuildConfigurationObject->ExcludeFiles = Functions::array_bc($data, 'exclude_files'); - } + + if(Functions::array_bc($data, 'pre_build') !== null) + $BuildConfigurationObject->PreBuild = Functions::array_bc($data, 'pre_build'); + + if(Functions::array_bc($data, 'post_build') !== null) + $BuildConfigurationObject->PostBuild = Functions::array_bc($data, 'post_build'); if(Functions::array_bc($data, 'dependencies') !== null) { foreach(Functions::array_bc($data, 'dependencies') as $item) - { $BuildConfigurationObject->Dependencies[] = Dependency::fromArray($item); - } } return $BuildConfigurationObject; diff --git a/src/ncc/Objects/ProjectConfiguration/ExecutionPolicy.php b/src/ncc/Objects/ProjectConfiguration/ExecutionPolicy.php new file mode 100644 index 0000000..b627a30 --- /dev/null +++ b/src/ncc/Objects/ProjectConfiguration/ExecutionPolicy.php @@ -0,0 +1,98 @@ + $this->Name, + ($bytecode ? Functions::cbc('runner') : 'runner') => $this->Runner, + ($bytecode ? Functions::cbc('message') : 'message') => $this->Message, + ($bytecode ? Functions::cbc('exec') : 'exec') => $this->Execute?->toArray($bytecode), + ($bytecode ? Functions::cbc('exit_handlers') : 'exit_handlers') => $this->ExitHandlers?->toArray($bytecode), + ]; + } + + /** + * @param array $data + * @return ExecutionPolicy + */ + public static function fromArray(array $data): self + { + $object = new self(); + + $object->Name = Functions::array_bc($data, 'name'); + $object->Runner = Functions::array_bc($data, 'runner'); + $object->Message = Functions::array_bc($data, 'message'); + $object->Execute = Functions::array_bc($data, 'exec'); + $object->ExitHandlers = Functions::array_bc($data, 'exit_handlers'); + + if($object->Execute !== null) + $object->Execute = Execute::fromArray($object->Execute); + + if($object->ExitHandlers !== null) + $object->ExitHandlers = ExitHandlers::fromArray($object->ExitHandlers); + + return $object; + } + } \ No newline at end of file diff --git a/src/ncc/Objects/ProjectConfiguration/ExecutionPolicy/Execute.php b/src/ncc/Objects/ProjectConfiguration/ExecutionPolicy/Execute.php new file mode 100644 index 0000000..0cc164a --- /dev/null +++ b/src/ncc/Objects/ProjectConfiguration/ExecutionPolicy/Execute.php @@ -0,0 +1,103 @@ +Tty = false; + $this->Silent = false; + $this->Timeout = null; + $this->WorkingDirectory = "%CWD%"; + } + + /** + * Returns an array representation of the object + * + * @param bool $bytecode + * @return array + */ + public function toArray(bool $bytecode=false): array + { + return [ + ($bytecode ? Functions::cbc('target') : 'target') => $this->Target, + ($bytecode ? Functions::cbc('working_directory') : 'working_directory') => $this->WorkingDirectory, + ($bytecode ? Functions::cbc('options') : 'options') => $this->Options, + ($bytecode ? Functions::cbc('silent') : 'silent') => $this->Silent, + ($bytecode ? Functions::cbc('tty') : 'tty') => $this->Tty, + ($bytecode ? Functions::cbc('timeout') : 'timeout') => $this->Timeout + ]; + } + + /** + * Constructs object from an array representation + * + * @param array $data + * @return Execute + */ + public static function fromArray(array $data): self + { + $object = new self(); + + $object->Target = Functions::array_bc($data, 'target'); + $object->WorkingDirectory = Functions::array_bc($data, 'working_directory'); + $object->Options = Functions::array_bc($data, 'options'); + $object->Silent = Functions::array_bc($data, 'silent'); + $object->Tty = Functions::array_bc($data, 'tty'); + $object->Timeout = Functions::array_bc($data, 'timeout'); + + return $object; + } + } \ No newline at end of file diff --git a/src/ncc/Objects/ProjectConfiguration/ExecutionPolicy/ExitHandle.php b/src/ncc/Objects/ProjectConfiguration/ExecutionPolicy/ExitHandle.php new file mode 100644 index 0000000..8d78bcc --- /dev/null +++ b/src/ncc/Objects/ProjectConfiguration/ExecutionPolicy/ExitHandle.php @@ -0,0 +1,76 @@ + $this->Message, + ($bytecode ? Functions::cbc('end_process') : 'end_process') => $this->EndProcess, + ($bytecode ? Functions::cbc('run') : 'run') => $this->Run, + ($bytecode ? Functions::cbc('exit_code') : 'exit_code') => $this->ExitCode, + ]; + } + + /** + * Constructs object from an array representation + * + * @param array $data + * @return ExitHandle + */ + public static function fromArray(array $data): self + { + $object = new self(); + + $object->Message = Functions::array_bc($data, 'message'); + $object->EndProcess = Functions::array_bc($data, 'end_process'); + $object->Run = Functions::array_bc($data, 'run'); + $object->ExitCode = Functions::array_bc($data, 'exit_code'); + + return $object; + } + } \ No newline at end of file diff --git a/src/ncc/Objects/ProjectConfiguration/ExecutionPolicy/ExitHandlers.php b/src/ncc/Objects/ProjectConfiguration/ExecutionPolicy/ExitHandlers.php new file mode 100644 index 0000000..28a73b2 --- /dev/null +++ b/src/ncc/Objects/ProjectConfiguration/ExecutionPolicy/ExitHandlers.php @@ -0,0 +1,71 @@ + $this->Success?->toArray($bytecode), + ($bytecode ? Functions::cbc('warning') : 'warning') => $this->Warning?->toArray($bytecode), + ($bytecode ? Functions::cbc('error') : 'error') => $this->Error?->toArray($bytecode), + ]; + } + + /** + * Constructs object from an array representation + * + * @param array $data + * @return ExitHandlers + */ + public static function fromArray(array $data): self + { + $object = new self(); + + $object->Success = Functions::array_bc($data, 'success'); + if($object->Success !== null) + $object->Success = ExitHandle::fromArray($object->Success); + + $object->Warning = Functions::array_bc($data, 'warning'); + if($object->Warning !== null) + $object->Warning = ExitHandle::fromArray($object->Warning); + + $object->Error = Functions::array_bc($data, 'error'); + if($object->Error !== null) + $object->Error = ExitHandle::fromArray($object->Error); + + return $object; + } + } \ No newline at end of file diff --git a/src/ncc/Objects/ProjectConfiguration/Installer.php b/src/ncc/Objects/ProjectConfiguration/Installer.php new file mode 100644 index 0000000..30dda88 --- /dev/null +++ b/src/ncc/Objects/ProjectConfiguration/Installer.php @@ -0,0 +1,88 @@ + $this->PreInstall, + ($bytecode? Functions::cbc('post_install') : 'post_install') => $this->PostInstall, + ($bytecode? Functions::cbc('pre_uninstall') : 'pre_uninstall') => $this->PostUninstall, + ($bytecode? Functions::cbc('post_uninstall') : 'post_uninstall') => $this->PostUninstall, + ($bytecode? Functions::cbc('pre_update') : 'pre_update') => $this->PreUpdate, + ($bytecode? Functions::cbc('post_update') : 'post_update') => $this->PostUpdate + ]; + } + + /** + * @param array $data + * @return Installer + */ + public static function fromArray(array $data): self + { + $object = new self(); + + $object->PreInstall = Functions::array_bc($data, 'pre_install'); + $object->PostInstall = Functions::array_bc($data, 'post_install'); + $object->PreUninstall = Functions::array_bc($data, 'pre_uninstall'); + $object->PostUninstall = Functions::array_bc($data, 'post_uninstall'); + $object->PreUpdate = Functions::array_bc($data, 'pre_update'); + $object->PostUpdate = Functions::array_bc($data, 'post_update'); + + return $object; + } + } \ No newline at end of file diff --git a/src/ncc/Utilities/Base64.php b/src/ncc/Utilities/Base64.php index f1d7b5e..9c75af3 100644 --- a/src/ncc/Utilities/Base64.php +++ b/src/ncc/Utilities/Base64.php @@ -17,6 +17,10 @@ */ public static function encode(string $string): string { + // Builtin function is faster than raw implementation + if(function_exists('base64_encode')) + return base64_encode($string); + $base64 = str_split('ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'); $bit_pattern = ''; $padding = 0; @@ -54,6 +58,9 @@ */ public static function decode(string $string): string { + if(function_exists('base64_decode')) + return base64_encode($string); + $base64 = str_split('ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'); $bit_pattern = ''; $padding = substr_count(substr(strrev($string), 0, 2), '='); diff --git a/src/ncc/Utilities/Console.php b/src/ncc/Utilities/Console.php index 870eeb9..30bd0ff 100644 --- a/src/ncc/Utilities/Console.php +++ b/src/ncc/Utilities/Console.php @@ -4,10 +4,17 @@ use Exception; use ncc\Abstracts\ConsoleColors; + use ncc\Abstracts\LogLevel; + use ncc\CLI\Main; use ncc\ncc; class Console { + /** + * @var int + */ + private static $largestTickLength = 0; + /** * Inline Progress bar, created by dealnews.com. * @@ -23,6 +30,20 @@ if(!ncc::cliMode()) return; + if(Main::getLogLevel() !== null) + { + switch(Main::getLogLevel()) + { + case LogLevel::Verbose: + case LogLevel::Debug: + case LogLevel::Silent: + return; + + default: + break; + } + } + static $start_time; // if we go over our bound, just ignore it @@ -44,6 +65,7 @@ $status_bar.="="; } + /** @noinspection PhpRedundantOptionalArgumentInspection */ $disp=number_format($perc*100, 0); $status_bar.=" ] $disp% $value/$total"; @@ -74,18 +96,54 @@ } } + /** + * Appends a verbose prefix to the message + * + * @param string $log_level + * @param string $input + * @return string + */ + private static function setPrefix(string $log_level, string $input): string + { + $input = match ($log_level) { + LogLevel::Verbose => self::formatColor('VRB:', ConsoleColors::LightCyan) . " $input", + LogLevel::Debug => self::formatColor('DBG:', ConsoleColors::LightMagenta) . " $input", + LogLevel::Info => self::formatColor('INF:', ConsoleColors::White) . " $input", + LogLevel::Warning => self::formatColor('WRN:', ConsoleColors::Yellow) . " $input", + LogLevel::Error => self::formatColor('ERR:', ConsoleColors::LightRed) . " $input", + LogLevel::Fatal => self::formatColor('FTL:', ConsoleColors::LightRed) . " $input", + default => self::formatColor('MSG:', ConsoleColors::Default) . " $input", + }; + + $tick_time = (string)microtime(true); + if(strlen($tick_time) > self::$largestTickLength) + self::$largestTickLength = strlen($tick_time); + if(strlen($tick_time) < self::$largestTickLength) + /** @noinspection PhpRedundantOptionalArgumentInspection */ + $tick_time = str_pad($tick_time, (strlen($tick_time) + (self::$largestTickLength - strlen($tick_time))), ' ', STR_PAD_RIGHT); + + return '[' . $tick_time . ' - ' . date('TH:i:sP') . '] ' . $input; + } + /** * Simple output function * * @param string $message * @param bool $newline + * @param bool $no_prefix * @return void */ - public static function out(string $message, bool $newline=true): void + public static function out(string $message, bool $newline=true, bool $no_prefix=false): void { if(!ncc::cliMode()) return; + if(Main::getLogLevel() !== null && !Resolver::checkLogLevel(LogLevel::Info, Main::getLogLevel())) + return; + + if(Main::getLogLevel() !== null && Resolver::checkLogLevel(LogLevel::Verbose, Main::getLogLevel()) && !$no_prefix) + $message = self::setPrefix(LogLevel::Info, $message); + if($newline) { print($message . PHP_EOL); @@ -95,6 +153,58 @@ print($message); } + /** + * Output debug message + * + * @param string $message + * @param bool $newline + * @return void + */ + public static function outDebug(string $message, bool $newline=true): void + { + if(!ncc::cliMode()) + return; + + if(Main::getLogLevel() !== null && !Resolver::checkLogLevel(LogLevel::Debug, Main::getLogLevel())) + return; + + $backtrace = null; + if(function_exists('debug_backtrace')) + $backtrace = debug_backtrace(); + $trace_msg = null; + if($backtrace !== null && isset($backtrace[1])) + { + $trace_msg = Console::formatColor($backtrace[1]['class'], ConsoleColors::LightGray); + $trace_msg .= $backtrace[1]['type']; + $trace_msg .= Console::formatColor($backtrace[1]['function'] . '()', ConsoleColors::LightGreen); + $trace_msg .= ' > '; + } + + /** @noinspection PhpUnnecessaryStringCastInspection */ + $message = self::setPrefix(LogLevel::Debug, (string)$trace_msg . $message); + + self::out($message, $newline, true); + } + + /** + * Output debug message + * + * @param string $message + * @param bool $newline + * @return void + */ + public static function outVerbose(string $message, bool $newline=true): void + { + if(!ncc::cliMode()) + return; + + if(Main::getLogLevel() !== null && !Resolver::checkLogLevel(LogLevel::Verbose, Main::getLogLevel())) + return; + + self::out(self::setPrefix(LogLevel::Verbose, $message), $newline, true); + } + + /** * Formats the text to have a different color and returns the formatted value * @@ -105,6 +215,11 @@ */ public static function formatColor(string $input, string $color_code, bool $persist=true): string { + if(Main::getArgs() !== null && isset(Main::getArgs()['no-color'])) + { + return $input; + } + if($persist) { return $color_code . $input . ConsoleColors::Default; @@ -125,6 +240,15 @@ if(!ncc::cliMode()) return; + if(Main::getLogLevel() !== null && !Resolver::checkLogLevel(LogLevel::Warning, Main::getLogLevel())) + return; + + if(Main::getLogLevel() !== null && Resolver::checkLogLevel(LogLevel::Verbose, Main::getLogLevel())) + { + self::out(self::setPrefix(LogLevel::Warning, $message), $newline, true); + return; + } + self::out(self::formatColor('Warning: ', ConsoleColors::Yellow) . $message, $newline); } @@ -141,7 +265,17 @@ if(!ncc::cliMode()) return; - self::out(self::formatColor(ConsoleColors::Red, 'Error: ') . $message, $newline); + if(Main::getLogLevel() !== null && !Resolver::checkLogLevel(LogLevel::Error, Main::getLogLevel())) + return; + + if(Main::getLogLevel() !== null && Resolver::checkLogLevel(LogLevel::Verbose, Main::getLogLevel())) + { + self::out(self::setPrefix(LogLevel::Error, $message), $newline, true); + } + else + { + self::out(self::formatColor(ConsoleColors::Red, 'Error: ') . $message, $newline); + } if($exit_code !== null) { @@ -162,11 +296,12 @@ if(!ncc::cliMode()) return; - if(strlen($message) > 0) + if(strlen($message) > 0 && Resolver::checkLogLevel(LogLevel::Error, Main::getLogLevel())) { - self::out(self::formatColor('Error: ' . $message, ConsoleColors::Red)); + self::out(PHP_EOL . self::formatColor('Error: ', ConsoleColors::Red) . $message); } + Console::out(PHP_EOL . '===== Exception Details ====='); self::outExceptionDetails($e); if($exit_code !== null) @@ -189,6 +324,39 @@ $trace_header = self::formatColor($e->getFile() . ':' . $e->getLine(), ConsoleColors::Magenta); $trace_error = self::formatColor('error: ', ConsoleColors::Red); self::out($trace_header . ' ' . $trace_error . $e->getMessage()); + self::out(sprintf('Error code: %s', $e->getCode())); + $trace = $e->getTrace(); + if(count($trace) > 1) + { + self::out('Stack Trace:'); + foreach($trace as $item) + { + self::out( ' - ' . self::formatColor($item['file'], ConsoleColors::Red) . ':' . $item['line']); + } + } + + if(Main::getArgs() !== null) + { + if(isset(Main::getArgs()['dbg-ex'])) + { + try + { + $dump = [ + 'constants' => ncc::getConstants(), + 'exception' => Functions::exceptionToArray($e) + ]; + IO::fwrite(getcwd() . DIRECTORY_SEPARATOR . time() . '.json', json_encode($dump, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT), 0777); + } + catch (Exception $e) + { + self::outWarning('Cannot dump exception details, ' . $e->getMessage()); + } + } + else + { + self::out('You can pass on \'--dbg-ex\' option to dump the exception details to a json file'); + } + } } /** diff --git a/src/ncc/Utilities/Functions.php b/src/ncc/Utilities/Functions.php index e0b74d9..1838950 100644 --- a/src/ncc/Utilities/Functions.php +++ b/src/ncc/Utilities/Functions.php @@ -2,9 +2,22 @@ namespace ncc\Utilities; + use Exception; + use ncc\Abstracts\Runners; + use ncc\Abstracts\Scopes; + use ncc\Classes\PhpExtension\Runner; + use ncc\Exceptions\AccessDeniedException; use ncc\Exceptions\FileNotFoundException; + use ncc\Exceptions\InvalidScopeException; + use ncc\Exceptions\IOException; use ncc\Exceptions\MalformedJsonException; + use ncc\Exceptions\UnsupportedRunnerException; + use ncc\Managers\CredentialManager; + use ncc\Managers\PackageLockManager; use ncc\Objects\CliHelpSection; + use ncc\Objects\Package\ExecutionUnit; + use ncc\Objects\ProjectConfiguration\ExecutionPolicy; + use ncc\ThirdParty\Symfony\Filesystem\Filesystem; /** * @author Zi Xing Narrakas @@ -22,11 +35,15 @@ * Calculates a byte-code representation of the input using CRC32 * * @param string $input - * @return int + * @return string */ - public static function cbc(string $input): int + public static function cbc(string $input): string { - return hexdec(hash('crc32', $input, true)); + $cache = RuntimeCache::get("cbc_$input"); + if($cache !== null) + return $cache; + + return RuntimeCache::set("cbc_$input", hash('crc32', $input, true)); } /** @@ -36,6 +53,7 @@ * @param array $data * @param string $select * @return mixed|null + * @noinspection PhpMissingReturnTypeInspection */ public static function array_bc(array $data, string $select) { @@ -54,7 +72,9 @@ * @param string $path * @param int $flags * @return mixed + * @throws AccessDeniedException * @throws FileNotFoundException + * @throws IOException * @throws MalformedJsonException * @noinspection PhpMissingReturnTypeInspection */ @@ -65,7 +85,7 @@ throw new FileNotFoundException($path); } - return self::loadJson(file_get_contents($path), $flags); + return self::loadJson(IO::fread($path), $flags); } /** @@ -98,6 +118,7 @@ * @return string * @throws MalformedJsonException * @noinspection PhpMissingParamTypeInspection + * @noinspection PhpUnusedLocalVariableInspection */ public static function encodeJson($value, int $flags=0): string { @@ -123,7 +144,7 @@ * @return void * @throws MalformedJsonException */ - public static function encodeJsonFile($value, string $path, int $flags=0) + public static function encodeJsonFile($value, string $path, int $flags=0): void { file_put_contents($path, self::encodeJson($value, $flags)); } @@ -160,16 +181,19 @@ * @param string $copyright * @param bool $basic_ascii * @return string + * @throws AccessDeniedException + * @throws FileNotFoundException + * @throws IOException */ public static function getBanner(string $version, string $copyright, bool $basic_ascii=false): string { if($basic_ascii) { - $banner = file_get_contents(__DIR__ . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR . 'banner_basic'); + $banner = IO::fread(__DIR__ . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR . 'banner_basic'); } else { - $banner = file_get_contents(__DIR__ . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR . 'banner_extended'); + $banner = IO::fread(__DIR__ . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR . 'banner_extended'); } $banner_version = str_pad($version, 21); @@ -219,26 +243,125 @@ } /** - * Converts the input string into a Bas64 encoding before returning it as a - * byte representation - * - * @param string $string - * @return string + * @param string $path + * @param ExecutionPolicy $policy + * @return ExecutionUnit + * @throws UnsupportedRunnerException + * @throws AccessDeniedException + * @throws FileNotFoundException + * @throws IOException */ - public static function byteEncode(string $string): string + public static function compileRunner(string $path, ExecutionPolicy $policy): ExecutionUnit { - return convert_uuencode(Base64::encode($string)); + return match (strtolower($policy->Runner)) { + Runners::php => Runner::processUnit($path, $policy), + default => throw new UnsupportedRunnerException('The runner \'' . $policy->Runner . '\' is not supported'), + }; } /** - * Decodes the input string back into the normal string representation that was encoded - * by the byteEncode() function + * Returns an array representation of the exception * - * @param string $string + * @param Exception $e + * @return array + */ + public static function exceptionToArray(Exception $e): array + { + $exception = [ + 'message' => $e->getMessage(), + 'code' => $e->getCode(), + 'file' => $e->getFile(), + 'line' => $e->getLine(), + 'trace' => null, + 'trace_string' => $e->getTraceAsString(), + ]; + + if($e->getPrevious() !== null) + { + $exception['trace'] = self::exceptionToArray($e); + } + + return $exception; + } + + /** + * Takes the input bytes and converts it to a readable unit representation + * + * @param int $bytes + * @param int $decimals * @return string */ - public static function byteDecode(string $string): string + public static function b2u(int $bytes, int $decimals=2): string { - return base64_decode(convert_uudecode($string)); + $size = array('B','kB','MB','GB','TB','PB','EB','ZB','YB'); + $factor = floor((strlen($bytes) - 1) / 3); + return sprintf("%.{$decimals}f", $bytes / pow(1024, $factor)) . @$size[$factor]; + } + + /** + * Initializes NCC files + * + * @return void + * @throws AccessDeniedException + * @throws InvalidScopeException + */ + public static function initializeFiles(): void + { + if(Resolver::resolveScope() !== Scopes::System) + throw new AccessDeniedException('Cannot initialize NCC files, insufficient permissions'); + + Console::outVerbose('Initializing NCC files'); + + $filesystem = new Filesystem(); + if(!$filesystem->exists(PathFinder::getDataPath(Scopes::System))) + { + Console::outDebug(sprintf('Initializing %s', PathFinder::getDataPath(Scopes::System))); + $filesystem->mkdir(PathFinder::getDataPath(Scopes::System), 0755); + } + + if(!$filesystem->exists(PathFinder::getCachePath(Scopes::System))) + { + Console::outDebug(sprintf('Initializing %s', PathFinder::getCachePath(Scopes::System))); + /** @noinspection PhpRedundantOptionalArgumentInspection */ + $filesystem->mkdir(PathFinder::getCachePath(Scopes::System), 0777); + } + + if(!$filesystem->exists(PathFinder::getRunnerPath(Scopes::System))) + { + Console::outDebug(sprintf('Initializing %s', PathFinder::getRunnerPath(Scopes::System))); + /** @noinspection PhpRedundantOptionalArgumentInspection */ + $filesystem->mkdir(PathFinder::getRunnerPath(Scopes::System), 0755); + } + + if(!$filesystem->exists(PathFinder::getPackagesPath(Scopes::System))) + { + Console::outDebug(sprintf('Initializing %s', PathFinder::getPackagesPath(Scopes::System))); + /** @noinspection PhpRedundantOptionalArgumentInspection */ + $filesystem->mkdir(PathFinder::getPackagesPath(Scopes::System), 0755); + } + + // Create credential store if needed + try + { + Console::outVerbose('Processing Credential Store'); + $credential_manager = new CredentialManager(); + $credential_manager->constructStore(); + } + catch (Exception $e) + { + Console::outError('Cannot construct credential store, ' . $e->getMessage() . ' (Error Code: ' . $e->getCode() . ')'); + } + + // Create package lock if needed + try + { + Console::outVerbose('Processing Package Lock'); + $package_manager = new PackageLockManager(); + $package_manager->constructLockFile(); + } + catch (Exception $e) + { + Console::outError('Cannot construct Package Lock, ' . $e->getMessage() . ' (Error Code: ' . $e->getCode() . ')'); + } } } \ No newline at end of file diff --git a/src/ncc/Utilities/IO.php b/src/ncc/Utilities/IO.php new file mode 100644 index 0000000..7b9c5be --- /dev/null +++ b/src/ncc/Utilities/IO.php @@ -0,0 +1,97 @@ +getPath())) + { + throw new IOException(sprintf('Attempted to write data to a directory instead of a file: (%s)', $uri)); + } + + Console::outDebug(sprintf('writing %s of data to %s', Functions::b2u(strlen($data)), $uri)); + $file = new SplFileObject($uri, $mode); + + if (!$file->flock(LOCK_EX | LOCK_NB)) + { + throw new IOException(sprintf('Unable to obtain lock on file: (%s)', $uri)); + } + elseif (!$file->fwrite($data)) + { + throw new IOException(sprintf('Unable to write content to file: (%s)... to (%s)', substr($data,0,25), $uri)); + } + elseif (!$file->flock(LOCK_UN)) + { + throw new IOException(sprintf('Unable to remove lock on file: (%s)', $uri)); + } + elseif (!@chmod($uri, $perms)) + { + throw new IOException(sprintf('Unable to chmod: (%s) to (%s)', $uri, $perms)); + } + } + + /** + * Attempts to read the specified file + * + * @param string $uri + * @param string $mode + * @param int|null $length + * @return string + * @throws AccessDeniedException + * @throws FileNotFoundException + * @throws IOException + */ + public static function fread(string $uri, string $mode='r', ?int $length=null): string + { + $fileInfo = new SplFileInfo($uri); + + if(!is_dir($fileInfo->getPath())) + { + throw new IOException(sprintf('Attempted to read data from a directory instead of a file: (%s)', $uri)); + } + + if(!file_exists($uri)) + { + throw new FileNotFoundException(sprintf('Cannot find file %s', $uri)); + } + + if(!is_readable($uri)) + { + throw new AccessDeniedException(sprintf('Insufficient permissions to read %s', $uri)); + } + + $file = new SplFileObject($uri, $mode); + if($length == null) + { + $length = $file->getSize(); + } + + if($length == 0) + { + return (string)null; + } + + Console::outDebug(sprintf('reading %s', $uri)); + return $file->fread($length); + } + } \ No newline at end of file diff --git a/src/ncc/Utilities/PathFinder.php b/src/ncc/Utilities/PathFinder.php index 373c0fa..5e08303 100644 --- a/src/ncc/Utilities/PathFinder.php +++ b/src/ncc/Utilities/PathFinder.php @@ -145,49 +145,16 @@ } /** - * Returns the path where temporary files are stored + * Returns the path where Runner bin files are located and installed * * @param string $scope * @param bool $win32 * @return string * @throws InvalidScopeException */ - public static function getTmpPath(string $scope=Scopes::Auto, bool $win32=false): string + public static function getRunnerPath(string $scope=Scopes::Auto, bool $win32=false): string { - return self::getDataPath($scope, $win32) . DIRECTORY_SEPARATOR . 'tmp'; - } - - /** - * Returns the configuration file - * - * @param string $scope - * @param bool $win32 - * @return string - * @throws InvalidScopeException - */ - public static function getConfigurationFile(string $scope=Scopes::Auto, bool $win32=false): string - { - return self::getDataPath($scope, $win32) . DIRECTORY_SEPARATOR . 'config'; - } - - /** - * Returns an array of all the configuration files the current user can access (For global-cross referencing) - * - * @param bool $win32 - * @return array - * @throws InvalidScopeException - */ - public static function getConfigurationFiles(bool $win32=false): array - { - $results = []; - $results[] = self::getConfigurationFile(Scopes::System, $win32); - - if(!in_array(self::getConfigurationFile(Scopes::User, $win32), $results)) - { - $results[] = self::getConfigurationFile(Scopes::User, $win32); - } - - return $results; + return self::getDataPath($scope, $win32) . DIRECTORY_SEPARATOR . 'runners'; } /** @@ -235,18 +202,4 @@ { return self::getDataPath($scope, $win32) . DIRECTORY_SEPARATOR . 'ext'; } - - /** - * Returns the file path where files for the given extension is stored - * - * @param string $extension_name - * @param string $scope - * @param bool $win32 - * @return string - * @throws InvalidScopeException - */ - public static function getNamedExtensionPath(string $extension_name, string $scope=Scopes::Auto, bool $win32=false): string - { - return self::getExtensionPath($scope, $win32) . DIRECTORY_SEPARATOR . Security::sanitizeFilename($extension_name); - } } \ No newline at end of file diff --git a/src/ncc/Utilities/Resolver.php b/src/ncc/Utilities/Resolver.php index 036248a..4d172a0 100644 --- a/src/ncc/Utilities/Resolver.php +++ b/src/ncc/Utilities/Resolver.php @@ -1,11 +1,21 @@ 4096) + return true; + + return false; + } } \ No newline at end of file diff --git a/tests/package_lock/load_package_lock.php b/tests/package_lock/load_package_lock.php new file mode 100644 index 0000000..258c388 --- /dev/null +++ b/tests/package_lock/load_package_lock.php @@ -0,0 +1,9 @@ +load(); + + var_dump($package_lock_manager->getPackageLock());