From c2b7be0ea297ea4967459b7c0fc8d42077b1c4ef Mon Sep 17 00:00:00 2001 From: Zi Xing Date: Sun, 29 Jan 2023 23:27:56 +0000 Subject: [PATCH] 1.0.0 Alpha Release --- .gitignore | 1 + .gitlab-ci.yml | 33 + .idea/copyright/Nosial.xml | 6 + .idea/copyright/profiles_settings.xml | 7 + .idea/ncc.iml | 2 + .idea/php.xml | 17 +- .idea/scopes/Autoloaders.xml | 3 + .idea/scopes/Installer_Source_files.xml | 3 + .idea/scopes/NCC_Source_files.xml | 2 +- .idea/scopes/Symfony_Filesystem.xml | 3 + .idea/scopes/Symfony_Process.xml | 3 + .idea/scopes/Symfony_Uid.xml | 3 + .idea/scopes/Symfony_Yaml.xml | 3 + .idea/scopes/Symfony_polyfill_ctype.xml | 3 + .idea/scopes/Symfony_polyfill_mbstring.xml | 3 + .idea/scopes/Symfony_polyfill_uuid.xml | 3 + .idea/scopes/defuse.xml | 3 + .idea/scopes/defuse_php_encryption.xml | 3 + .idea/scopes/jelix.xml | 3 + .idea/scopes/jelix_version.xml | 3 + .idea/scopes/nikic.xml | 3 + .idea/scopes/nikic_PhpParser.xml | 3 + .idea/scopes/theseer.xml | 3 + .idea/scopes/theseer_Autoload.xml | 3 + .idea/scopes/theseer_DirectoryScanner.xml | 3 + CODE_OF_CONDUCT.md | 36 + CONTRIBUTING.md | 25 + DOCUMENTATION.md | 851 +++++++++++++++++- LICENSE | 363 +------- Makefile | 31 +- README.md | 68 +- assets/ncc_cli.png | Bin 0 -> 92689 bytes changelog/v1.0.0_alpha.md | 18 + docs/building_ncc.md | 94 -- docs/package_name.md | 43 - .../project_configuration/execution_policy.md | 151 ---- src/config/ncc.yaml | 10 +- .../custom_repositories.json | 6 + src/default_repositories/gitgud.json | 6 + src/default_repositories/github.json | 6 + src/default_repositories/gitlab.json | 6 + src/default_repositories/n64.json | 6 + src/installer/extension | 161 +++- src/installer/installer | 126 ++- src/ncc/Abstracts/AuthenticationSource.php | 12 - src/ncc/Abstracts/AuthenticationType.php | 36 + src/ncc/Abstracts/BuiltinRemoteSourceType.php | 37 + .../CompilerExtensionDefaultVersions.php | 24 +- .../CompilerExtensionSupportedVersions.php | 24 +- src/ncc/Abstracts/CompilerExtensions.php | 22 +- src/ncc/Abstracts/CompilerOptions.php | 8 - src/ncc/Abstracts/ComponentDataType.php | 22 +- src/ncc/Abstracts/ComponentFileExtensions.php | 22 +- src/ncc/Abstracts/ComposerPackageTypes.php | 24 +- src/ncc/Abstracts/ComposerStabilityTypes.php | 22 +- src/ncc/Abstracts/ConsoleColors.php | 22 +- src/ncc/Abstracts/ConstantReferences.php | 24 +- src/ncc/Abstracts/DefinedRemoteSourceType.php | 53 ++ src/ncc/Abstracts/DependencySourceType.php | 22 +- src/ncc/Abstracts/EncoderType.php | 22 +- src/ncc/Abstracts/ExceptionCodes.php | 126 ++- src/ncc/Abstracts/HttpRequestType.php | 31 + src/ncc/Abstracts/HttpStatusCodes.php | 124 +++ src/ncc/Abstracts/LogLevel.php | 22 +- src/ncc/Abstracts/NccBuildFlags.php | 24 +- .../Options/BuildConfigurationValues.php | 22 +- .../Options/InitializeProjectOptions.php | 22 +- .../Options/InstallPackageOptions.php | 40 + .../Options/RuntimeImportOptions.php | 38 + src/ncc/Abstracts/PackageStandardVersions.php | 22 +- .../Abstracts/PackageStructureVersions.php | 22 +- src/ncc/Abstracts/ProjectType.php | 32 + src/ncc/Abstracts/RegexPatterns.php | 22 +- .../Abstracts/RemoteAuthenticationType.php | 16 - src/ncc/Abstracts/RemoteSource.php | 16 - src/ncc/Abstracts/RemoteSourceType.php | 50 + src/ncc/Abstracts/Runners.php | 45 +- src/ncc/Abstracts/Scopes.php | 22 +- .../SpecialConstants/AssemblyConstants.php | 22 +- .../SpecialConstants/BuildConstants.php | 22 +- .../SpecialConstants/DateTimeConstants.php | 22 +- .../SpecialConstants/InstallConstants.php | 22 +- .../SpecialConstants/RuntimeConstants.php | 27 +- src/ncc/Abstracts/StringPaddingMethod.php | 12 - src/ncc/Abstracts/Versions.php | 27 +- .../BuildCommand.php} | 27 +- src/ncc/CLI/Commands/ExecCommand.php | 144 +++ src/ncc/CLI/CredentialMenu.php | 72 -- src/ncc/CLI/HelpMenu.php | 98 +- src/ncc/CLI/Main.php | 105 ++- src/ncc/CLI/{ => Management}/ConfigMenu.php | 24 +- src/ncc/CLI/Management/CredentialMenu.php | 397 ++++++++ .../{ => Management}/PackageManagerMenu.php | 269 +++++- src/ncc/CLI/{ => Management}/ProjectMenu.php | 48 +- src/ncc/CLI/Management/SourcesMenu.php | 240 +++++ src/ncc/CLI/PhpMenu.php | 59 -- src/ncc/Classes/BashExtension/BashRunner.php | 56 ++ ...erSource.php => ComposerSourceBuiltin.php} | 540 +++++++---- src/ncc/Classes/EnvironmentConfiguration.php | 71 -- src/ncc/Classes/GitClient.php | 132 +++ .../Classes/GithubExtension/GithubService.php | 277 ++++++ .../Classes/GitlabExtension/GitlabService.php | 242 +++++ src/ncc/Classes/HttpClient.php | 241 +++++ src/ncc/Classes/LuaExtension/LuaRunner.php | 53 ++ .../Classes/NccExtension/ConstantCompiler.php | 49 +- .../Classes/NccExtension/PackageCompiler.php | 159 +++- src/ncc/Classes/NccExtension/Runner.php | 35 +- src/ncc/Classes/PerlExtension/PerlRunner.php | 56 ++ src/ncc/Classes/PhpExtension/PhpCompiler.php | 162 ++-- src/ncc/Classes/PhpExtension/PhpInstaller.php | 48 +- src/ncc/Classes/PhpExtension/PhpRunner.php | 51 +- src/ncc/Classes/PhpExtension/PhpRuntime.php | 124 +++ .../Classes/PythonExtension/Python2Runner.php | 56 ++ .../Classes/PythonExtension/Python3Runner.php | 56 ++ .../Classes/PythonExtension/PythonRunner.php | 56 ++ src/ncc/Exceptions/AccessDeniedException.php | 22 +- src/ncc/Exceptions/ArchiveException.php | 39 + .../Exceptions/AuthenticationException.php | 39 + .../Exceptions/AutoloadGeneratorException.php | 22 +- .../BuildConfigurationNotFoundException.php | 28 +- src/ncc/Exceptions/BuildException.php | 31 +- .../Exceptions/ComponentChecksumException.php | 30 +- .../Exceptions/ComponentDecodeException.php | 31 +- .../ComponentVersionNotFoundException.php | 22 +- .../Exceptions/ComposerDisabledException.php | 34 +- src/ncc/Exceptions/ComposerException.php | 31 +- .../ComposerNotAvailableException.php | 32 +- .../Exceptions/ConstantReadonlyException.php | 22 +- .../Exceptions/DirectoryNotFoundException.php | 23 +- .../ExecutionUnitNotFoundException.php | 8 - src/ncc/Exceptions/FileNotFoundException.php | 23 +- src/ncc/Exceptions/GitCheckoutException.php | 39 + src/ncc/Exceptions/GitCloneException.php | 39 + src/ncc/Exceptions/GitTagsException.php | 39 + src/ncc/Exceptions/GithubServiceException.php | 39 + src/ncc/Exceptions/GitlabServiceException.php | 39 + src/ncc/Exceptions/HttpException.php | 39 + src/ncc/Exceptions/IOException.php | 31 +- src/ncc/Exceptions/ImportException.php | 39 + src/ncc/Exceptions/InstallationException.php | 31 +- .../InternalComposerNotAvailableException.php | 31 +- .../InvalidBuildConfigurationException.php | 39 + .../InvalidConstantNameException.php | 31 +- .../InvalidCredentialsEntryException.php | 22 +- .../InvalidDependencyConfiguration.php | 39 + .../Exceptions/InvalidExecutionPolicyName.php | 29 +- .../Exceptions/InvalidPackageException.php | 28 +- .../InvalidPackageNameException.php | 22 +- .../InvalidProjectBuildConfiguration.php | 28 +- .../InvalidProjectConfigurationException.php | 40 +- .../InvalidProjectNameException.php | 24 +- .../InvalidPropertyValueException.php | 36 +- src/ncc/Exceptions/InvalidScopeException.php | 24 +- .../InvalidVersionConfigurationException.php | 31 +- .../InvalidVersionNumberException.php | 22 +- src/ncc/Exceptions/MalformedJsonException.php | 22 +- .../Exceptions/MissingDependencyException.php | 31 +- .../Exceptions/NoAvailableUnitsException.php | 31 +- src/ncc/Exceptions/NoUnitsFoundException.php | 29 +- .../Exceptions/NotImplementedException.php | 31 +- src/ncc/Exceptions/NotSupportedException.php | 39 + .../PackageAlreadyInstalledException.php | 31 +- src/ncc/Exceptions/PackageFetchException.php | 39 + src/ncc/Exceptions/PackageLockException.php | 31 +- .../Exceptions/PackageNotFoundException.php | 31 +- .../Exceptions/PackageParsingException.php | 31 +- .../PackagePreparationFailedException.php | 31 +- .../ProjectAlreadyExistsException.php | 22 +- .../ProjectConfigurationNotFoundException.php | 30 +- .../Exceptions/ResourceChecksumException.php | 32 +- .../Exceptions/RunnerExecutionException.php | 31 +- src/ncc/Exceptions/RuntimeException.php | 22 +- src/ncc/Exceptions/SymlinkException.php | 39 + .../UndefinedExecutionPolicyException.php | 31 +- .../UnsupportedArchiveException.php | 39 + .../UnsupportedCompilerExtensionException.php | 31 +- .../UnsupportedComponentTypeException.php | 31 +- .../UnsupportedExtensionVersionException.php | 32 +- .../UnsupportedPackageException.php | 28 +- .../UnsupportedProjectTypeException.php | 40 + .../UnsupportedRemoteSourceTypeException.php | 39 + .../Exceptions/UnsupportedRunnerException.php | 11 - .../UserAbortedOperationException.php | 31 +- .../Exceptions/VersionNotFoundException.php | 31 +- .../Extensions/ZiProto/Abstracts/Options.php | 22 +- .../Extensions/ZiProto/Abstracts/Regex.php | 24 +- src/ncc/Extensions/ZiProto/BufferStream.php | 33 +- .../Extensions/ZiProto/DecodingOptions.php | 30 +- .../Extensions/ZiProto/EncodingOptions.php | 22 +- .../Exception/DecodingFailedException.php | 22 +- .../Exception/EncodingFailedException.php | 22 +- .../Exception/InsufficientDataException.php | 22 +- .../Exception/IntegerOverflowException.php | 22 +- .../Exception/InvalidOptionException.php | 22 +- src/ncc/Extensions/ZiProto/Ext.php | 23 +- src/ncc/Extensions/ZiProto/Packet.php | 50 +- src/ncc/Extensions/ZiProto/Type/Binary.php | 22 +- src/ncc/Extensions/ZiProto/Type/Map.php | 22 +- .../TypeTransformer/BinaryTransformer.php | 24 +- .../ZiProto/TypeTransformer/Extension.php | 24 +- .../TypeTransformer/MapTransformer.php | 23 +- .../ZiProto/TypeTransformer/Validator.php | 23 +- src/ncc/Extensions/ZiProto/ZiProto.php | 22 +- src/ncc/Interfaces/CompilerInterface.php | 25 +- src/ncc/Interfaces/InstallerInterface.php | 22 +- src/ncc/Interfaces/PasswordInterface.php | 48 + src/ncc/Interfaces/RemoteSourceInterface.php | 18 - .../Interfaces/RepositorySourceInterface.php | 61 ++ src/ncc/Interfaces/RunnerInterface.php | 34 +- src/ncc/Interfaces/RuntimeInterface.php | 43 + src/ncc/Interfaces/ServiceSourceInterface.php | 38 + src/ncc/Managers/ConfigurationManager.php | 235 +---- src/ncc/Managers/CredentialManager.php | 152 ++-- src/ncc/Managers/ExecutionPointerManager.php | 163 +++- src/ncc/Managers/PackageLockManager.php | 34 +- src/ncc/Managers/PackageManager.php | 414 ++++++++- src/ncc/Managers/ProjectManager.php | 42 +- src/ncc/Managers/RemoteSourcesManager.php | 171 ++++ src/ncc/Managers/SymlinkManager.php | 355 ++++++++ src/ncc/Objects/CliHelpSection.php | 20 + src/ncc/Objects/ComposerJson.php | 20 + src/ncc/Objects/ComposerJson/Author.php | 20 + src/ncc/Objects/ComposerJson/Autoloader.php | 20 + src/ncc/Objects/ComposerJson/Funding.php | 22 +- .../Objects/ComposerJson/NamespacePointer.php | 22 +- src/ncc/Objects/ComposerJson/PackageLink.php | 22 +- src/ncc/Objects/ComposerJson/Suggestion.php | 22 +- src/ncc/Objects/ComposerJson/Support.php | 22 +- src/ncc/Objects/ComposerLock.php | 20 + src/ncc/Objects/Constant.php | 24 +- src/ncc/Objects/DefinedRemoteSource.php | 95 ++ src/ncc/Objects/ExecutionPointers.php | 43 +- .../ExecutionPointers/ExecutionPointer.php | 24 + src/ncc/Objects/HttpRequest.php | 128 +++ src/ncc/Objects/HttpResponse.php | 70 ++ src/ncc/Objects/HttpResponseCache.php | 74 ++ src/ncc/Objects/InstallationPaths.php | 20 + src/ncc/Objects/NccUpdateInformation.php | 20 + src/ncc/Objects/NccVersionInformation.php | 20 + .../NccVersionInformation/Component.php | 20 + src/ncc/Objects/Package.php | 58 ++ src/ncc/Objects/Package/Component.php | 20 + src/ncc/Objects/Package/ExecutionUnit.php | 23 + src/ncc/Objects/Package/Header.php | 34 +- src/ncc/Objects/Package/Installer.php | 20 + src/ncc/Objects/Package/MagicBytes.php | 20 + src/ncc/Objects/Package/Resource.php | 20 + src/ncc/Objects/PackageLock.php | 75 +- .../Objects/PackageLock/DependencyEntry.php | 20 + src/ncc/Objects/PackageLock/PackageEntry.php | 64 ++ src/ncc/Objects/PackageLock/VersionEntry.php | 22 +- src/ncc/Objects/PhpConfiguration.php | 20 + src/ncc/Objects/ProjectConfiguration.php | 59 +- .../Objects/ProjectConfiguration/Assembly.php | 20 + .../Objects/ProjectConfiguration/Build.php | 97 +- .../{ => Build}/BuildConfiguration.php | 111 ++- .../Objects/ProjectConfiguration/Compiler.php | 23 +- .../ProjectConfiguration/Dependency.php | 53 +- .../ProjectConfiguration/ExecutionPolicy.php | 26 +- .../ExecutionPolicy/Execute.php | 49 +- .../ExecutionPolicy/ExitHandle.php | 20 + .../ExecutionPolicy/ExitHandlers.php | 20 + .../ProjectConfiguration/Installer.php | 20 + .../Objects/ProjectConfiguration/Project.php | 35 +- .../ProjectConfiguration/UpdateSource.php | 72 +- .../UpdateSource/Repository.php | 90 ++ src/ncc/Objects/ProjectDetectionResults.php | 78 ++ src/ncc/Objects/RemotePackageInput.php | 20 + src/ncc/Objects/RepositoryQueryResults.php | 66 ++ .../Objects/RepositoryQueryResults/Files.php | 70 ++ src/ncc/Objects/SymlinkDictionary.php | 28 + .../SymlinkDictionary/SymlinkEntry.php | 93 ++ src/ncc/Objects/Vault.php | 192 +++- src/ncc/Objects/Vault/DefaultEntry.php | 60 -- src/ncc/Objects/Vault/Entry.php | 384 ++++++-- .../Objects/Vault/Password/AccessToken.php | 102 +++ .../Vault/Password/UsernamePassword.php | 129 +++ src/ncc/Runtime.php | 228 +++++ src/ncc/Runtime/Constants.php | 40 + src/ncc/Utilities/Base64.php | 22 +- src/ncc/Utilities/Console.php | 128 ++- src/ncc/Utilities/Functions.php | 549 ++++++++++- src/ncc/Utilities/IO.php | 22 +- src/ncc/Utilities/PathFinder.php | 91 +- src/ncc/Utilities/Resolver.php | 91 +- src/ncc/Utilities/RuntimeCache.php | 50 +- src/ncc/Utilities/Security.php | 29 +- src/ncc/Utilities/Validate.php | 59 +- src/ncc/ncc | 26 +- src/ncc/ncc.php | 39 +- src/ncc/version.json | 4 +- tests/example_project/scripts/unit.bash | 3 + tests/example_project/scripts/unit.lua | 4 + tests/example_project/scripts/unit.php | 5 + tests/example_project/scripts/unit.pl | 6 + tests/example_project/scripts/unit.py2 | 3 + tests/example_project/scripts/unit.py3 | 3 + 297 files changed, 13712 insertions(+), 2517 deletions(-) create mode 100644 .gitlab-ci.yml create mode 100644 .idea/copyright/Nosial.xml create mode 100644 .idea/copyright/profiles_settings.xml create mode 100644 .idea/scopes/Autoloaders.xml create mode 100644 .idea/scopes/Installer_Source_files.xml create mode 100644 .idea/scopes/Symfony_Filesystem.xml create mode 100644 .idea/scopes/Symfony_Process.xml create mode 100644 .idea/scopes/Symfony_Uid.xml create mode 100644 .idea/scopes/Symfony_Yaml.xml create mode 100644 .idea/scopes/Symfony_polyfill_ctype.xml create mode 100644 .idea/scopes/Symfony_polyfill_mbstring.xml create mode 100644 .idea/scopes/Symfony_polyfill_uuid.xml create mode 100644 .idea/scopes/defuse.xml create mode 100644 .idea/scopes/defuse_php_encryption.xml create mode 100644 .idea/scopes/jelix.xml create mode 100644 .idea/scopes/jelix_version.xml create mode 100644 .idea/scopes/nikic.xml create mode 100644 .idea/scopes/nikic_PhpParser.xml create mode 100644 .idea/scopes/theseer.xml create mode 100644 .idea/scopes/theseer_Autoload.xml create mode 100644 .idea/scopes/theseer_DirectoryScanner.xml create mode 100644 CODE_OF_CONDUCT.md create mode 100644 CONTRIBUTING.md create mode 100644 assets/ncc_cli.png create mode 100644 changelog/v1.0.0_alpha.md delete mode 100644 docs/building_ncc.md delete mode 100644 docs/package_name.md delete mode 100644 docs/project_configuration/execution_policy.md create mode 100644 src/default_repositories/custom_repositories.json create mode 100644 src/default_repositories/gitgud.json create mode 100644 src/default_repositories/github.json create mode 100644 src/default_repositories/gitlab.json create mode 100644 src/default_repositories/n64.json delete mode 100644 src/ncc/Abstracts/AuthenticationSource.php create mode 100644 src/ncc/Abstracts/AuthenticationType.php create mode 100644 src/ncc/Abstracts/BuiltinRemoteSourceType.php delete mode 100644 src/ncc/Abstracts/CompilerOptions.php create mode 100644 src/ncc/Abstracts/DefinedRemoteSourceType.php create mode 100644 src/ncc/Abstracts/HttpRequestType.php create mode 100644 src/ncc/Abstracts/HttpStatusCodes.php create mode 100644 src/ncc/Abstracts/Options/InstallPackageOptions.php create mode 100644 src/ncc/Abstracts/Options/RuntimeImportOptions.php create mode 100644 src/ncc/Abstracts/ProjectType.php delete mode 100644 src/ncc/Abstracts/RemoteAuthenticationType.php delete mode 100644 src/ncc/Abstracts/RemoteSource.php create mode 100644 src/ncc/Abstracts/RemoteSourceType.php delete mode 100644 src/ncc/Abstracts/StringPaddingMethod.php rename src/ncc/CLI/{BuildMenu.php => Commands/BuildCommand.php} (71%) create mode 100644 src/ncc/CLI/Commands/ExecCommand.php delete mode 100644 src/ncc/CLI/CredentialMenu.php rename src/ncc/CLI/{ => Management}/ConfigMenu.php (79%) create mode 100644 src/ncc/CLI/Management/CredentialMenu.php rename src/ncc/CLI/{ => Management}/PackageManagerMenu.php (65%) rename src/ncc/CLI/{ => Management}/ProjectMenu.php (80%) create mode 100644 src/ncc/CLI/Management/SourcesMenu.php delete mode 100644 src/ncc/CLI/PhpMenu.php create mode 100644 src/ncc/Classes/BashExtension/BashRunner.php rename src/ncc/Classes/ComposerExtension/{ComposerSource.php => ComposerSourceBuiltin.php} (54%) delete mode 100644 src/ncc/Classes/EnvironmentConfiguration.php create mode 100644 src/ncc/Classes/GitClient.php create mode 100644 src/ncc/Classes/GithubExtension/GithubService.php create mode 100644 src/ncc/Classes/GitlabExtension/GitlabService.php create mode 100644 src/ncc/Classes/HttpClient.php create mode 100644 src/ncc/Classes/LuaExtension/LuaRunner.php create mode 100644 src/ncc/Classes/PerlExtension/PerlRunner.php create mode 100644 src/ncc/Classes/PhpExtension/PhpRuntime.php create mode 100644 src/ncc/Classes/PythonExtension/Python2Runner.php create mode 100644 src/ncc/Classes/PythonExtension/Python3Runner.php create mode 100644 src/ncc/Classes/PythonExtension/PythonRunner.php create mode 100644 src/ncc/Exceptions/ArchiveException.php create mode 100644 src/ncc/Exceptions/AuthenticationException.php delete mode 100644 src/ncc/Exceptions/ExecutionUnitNotFoundException.php create mode 100644 src/ncc/Exceptions/GitCheckoutException.php create mode 100644 src/ncc/Exceptions/GitCloneException.php create mode 100644 src/ncc/Exceptions/GitTagsException.php create mode 100644 src/ncc/Exceptions/GithubServiceException.php create mode 100644 src/ncc/Exceptions/GitlabServiceException.php create mode 100644 src/ncc/Exceptions/HttpException.php create mode 100644 src/ncc/Exceptions/ImportException.php create mode 100644 src/ncc/Exceptions/InvalidBuildConfigurationException.php create mode 100644 src/ncc/Exceptions/InvalidDependencyConfiguration.php create mode 100644 src/ncc/Exceptions/NotSupportedException.php create mode 100644 src/ncc/Exceptions/PackageFetchException.php create mode 100644 src/ncc/Exceptions/SymlinkException.php create mode 100644 src/ncc/Exceptions/UnsupportedArchiveException.php create mode 100644 src/ncc/Exceptions/UnsupportedProjectTypeException.php create mode 100644 src/ncc/Exceptions/UnsupportedRemoteSourceTypeException.php delete mode 100644 src/ncc/Exceptions/UnsupportedRunnerException.php create mode 100644 src/ncc/Interfaces/PasswordInterface.php delete mode 100644 src/ncc/Interfaces/RemoteSourceInterface.php create mode 100644 src/ncc/Interfaces/RepositorySourceInterface.php create mode 100644 src/ncc/Interfaces/RuntimeInterface.php create mode 100644 src/ncc/Interfaces/ServiceSourceInterface.php create mode 100644 src/ncc/Managers/RemoteSourcesManager.php create mode 100644 src/ncc/Managers/SymlinkManager.php create mode 100644 src/ncc/Objects/DefinedRemoteSource.php create mode 100644 src/ncc/Objects/HttpRequest.php create mode 100644 src/ncc/Objects/HttpResponse.php create mode 100644 src/ncc/Objects/HttpResponseCache.php rename src/ncc/Objects/ProjectConfiguration/{ => Build}/BuildConfiguration.php (53%) create mode 100644 src/ncc/Objects/ProjectConfiguration/UpdateSource/Repository.php create mode 100644 src/ncc/Objects/ProjectDetectionResults.php create mode 100644 src/ncc/Objects/RepositoryQueryResults.php create mode 100644 src/ncc/Objects/RepositoryQueryResults/Files.php create mode 100644 src/ncc/Objects/SymlinkDictionary.php create mode 100644 src/ncc/Objects/SymlinkDictionary/SymlinkEntry.php delete mode 100644 src/ncc/Objects/Vault/DefaultEntry.php create mode 100644 src/ncc/Objects/Vault/Password/AccessToken.php create mode 100644 src/ncc/Objects/Vault/Password/UsernamePassword.php create mode 100644 src/ncc/Runtime.php create mode 100644 tests/example_project/scripts/unit.bash create mode 100644 tests/example_project/scripts/unit.lua create mode 100644 tests/example_project/scripts/unit.php create mode 100644 tests/example_project/scripts/unit.pl create mode 100644 tests/example_project/scripts/unit.py2 create mode 100644 tests/example_project/scripts/unit.py3 diff --git a/.gitignore b/.gitignore index 570cd0f..4e93986 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,7 @@ build src/ncc/ThirdParty/defuse/php-encryption/autoload_spl.php src/ncc/ThirdParty/jelix/version/autoload_spl.php src/ncc/ThirdParty/nikic/PhpParser/autoload_spl.php +src/ncc/ThirdParty/php_parallel_lint/php_console_color/autoload_spl.php src/ncc/ThirdParty/Symfony/polyfill-ctype/autoload_spl.php src/ncc/ThirdParty/Symfony/polyfill-mbstring/autoload_spl.php src/ncc/ThirdParty/Symfony/polyfill-uuid/autoload_spl.php diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 0000000..7a5e944 --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,33 @@ +image: php:8.1 + +before_script: + # Install some stuff that the image doesn't come with + - apt update -yqq + - apt install git libpq-dev libzip-dev zip make wget gnupg -yqq + + # Install phive + - wget -O phive.phar https://phar.io/releases/phive.phar + - wget -O phive.phar.asc https://phar.io/releases/phive.phar.asc + - gpg --keyserver hkps://keys.openpgp.org --recv-keys 0x9D8A98B29B2D5D79 + - gpg --verify phive.phar.asc phive.phar + - chmod +x phive.phar + - mv phive.phar /usr/local/bin/phive + + # install phpab + - phive install phpab --global --trust-gpg-keys 0x2A8299CE842DD38C + +build: + script: + - make tar + rules: + - if: $CI_COMMIT_BRANCH + +release: + script: + - make tar + - mv build/build.tar.gz build/ncc_$CI_COMMIT_TAG.tar.gz + artifacts: + paths: + - build/ncc_$CI_COMMIT_TAG.tar.gz + rules: + - if: $CI_COMMIT_TAG \ No newline at end of file diff --git a/.idea/copyright/Nosial.xml b/.idea/copyright/Nosial.xml new file mode 100644 index 0000000..ea25c5d --- /dev/null +++ b/.idea/copyright/Nosial.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/.idea/copyright/profiles_settings.xml b/.idea/copyright/profiles_settings.xml new file mode 100644 index 0000000..85fcaf5 --- /dev/null +++ b/.idea/copyright/profiles_settings.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/.idea/ncc.iml b/.idea/ncc.iml index 2c5c861..f554332 100644 --- a/.idea/ncc.iml +++ b/.idea/ncc.iml @@ -8,6 +8,8 @@ + + diff --git a/.idea/php.xml b/.idea/php.xml index a87bd36..dec4919 100644 --- a/.idea/php.xml +++ b/.idea/php.xml @@ -1,11 +1,26 @@ + + + + + + - + + + + + \ No newline at end of file diff --git a/.idea/scopes/Autoloaders.xml b/.idea/scopes/Autoloaders.xml new file mode 100644 index 0000000..e193470 --- /dev/null +++ b/.idea/scopes/Autoloaders.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/.idea/scopes/Installer_Source_files.xml b/.idea/scopes/Installer_Source_files.xml new file mode 100644 index 0000000..f4cf65d --- /dev/null +++ b/.idea/scopes/Installer_Source_files.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/.idea/scopes/NCC_Source_files.xml b/.idea/scopes/NCC_Source_files.xml index 6e38ebb..a65b319 100644 --- a/.idea/scopes/NCC_Source_files.xml +++ b/.idea/scopes/NCC_Source_files.xml @@ -1,3 +1,3 @@ - + \ No newline at end of file diff --git a/.idea/scopes/Symfony_Filesystem.xml b/.idea/scopes/Symfony_Filesystem.xml new file mode 100644 index 0000000..00b6846 --- /dev/null +++ b/.idea/scopes/Symfony_Filesystem.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/.idea/scopes/Symfony_Process.xml b/.idea/scopes/Symfony_Process.xml new file mode 100644 index 0000000..7d46ad8 --- /dev/null +++ b/.idea/scopes/Symfony_Process.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/.idea/scopes/Symfony_Uid.xml b/.idea/scopes/Symfony_Uid.xml new file mode 100644 index 0000000..0b4c6e4 --- /dev/null +++ b/.idea/scopes/Symfony_Uid.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/.idea/scopes/Symfony_Yaml.xml b/.idea/scopes/Symfony_Yaml.xml new file mode 100644 index 0000000..d2796d6 --- /dev/null +++ b/.idea/scopes/Symfony_Yaml.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/.idea/scopes/Symfony_polyfill_ctype.xml b/.idea/scopes/Symfony_polyfill_ctype.xml new file mode 100644 index 0000000..93bc242 --- /dev/null +++ b/.idea/scopes/Symfony_polyfill_ctype.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/.idea/scopes/Symfony_polyfill_mbstring.xml b/.idea/scopes/Symfony_polyfill_mbstring.xml new file mode 100644 index 0000000..9125e7b --- /dev/null +++ b/.idea/scopes/Symfony_polyfill_mbstring.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/.idea/scopes/Symfony_polyfill_uuid.xml b/.idea/scopes/Symfony_polyfill_uuid.xml new file mode 100644 index 0000000..fc04e03 --- /dev/null +++ b/.idea/scopes/Symfony_polyfill_uuid.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/.idea/scopes/defuse.xml b/.idea/scopes/defuse.xml new file mode 100644 index 0000000..3c1064b --- /dev/null +++ b/.idea/scopes/defuse.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/.idea/scopes/defuse_php_encryption.xml b/.idea/scopes/defuse_php_encryption.xml new file mode 100644 index 0000000..bff7e6a --- /dev/null +++ b/.idea/scopes/defuse_php_encryption.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/.idea/scopes/jelix.xml b/.idea/scopes/jelix.xml new file mode 100644 index 0000000..6e134aa --- /dev/null +++ b/.idea/scopes/jelix.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/.idea/scopes/jelix_version.xml b/.idea/scopes/jelix_version.xml new file mode 100644 index 0000000..89f8c5e --- /dev/null +++ b/.idea/scopes/jelix_version.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/.idea/scopes/nikic.xml b/.idea/scopes/nikic.xml new file mode 100644 index 0000000..471928a --- /dev/null +++ b/.idea/scopes/nikic.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/.idea/scopes/nikic_PhpParser.xml b/.idea/scopes/nikic_PhpParser.xml new file mode 100644 index 0000000..22f7509 --- /dev/null +++ b/.idea/scopes/nikic_PhpParser.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/.idea/scopes/theseer.xml b/.idea/scopes/theseer.xml new file mode 100644 index 0000000..c46f520 --- /dev/null +++ b/.idea/scopes/theseer.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/.idea/scopes/theseer_Autoload.xml b/.idea/scopes/theseer_Autoload.xml new file mode 100644 index 0000000..f64a6e8 --- /dev/null +++ b/.idea/scopes/theseer_Autoload.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/.idea/scopes/theseer_DirectoryScanner.xml b/.idea/scopes/theseer_DirectoryScanner.xml new file mode 100644 index 0000000..72d0ec3 --- /dev/null +++ b/.idea/scopes/theseer_DirectoryScanner.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..f7a58e1 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,36 @@ + +# Code of Conduct + +We are committed to maintaining a welcoming and inclusive environment for all contributors. In order to ensure that +everyone feels safe and respected, we have established the following code of conduct. + +## Our Standards + +We expect all contributors to: + +- Be respectful and considerate of others +- Use inclusive language +- Avoid demeaning, discriminatory, or harassing behavior +- Respect the boundaries of others + +We will not tolerate any behavior that does not align with these standards. + +## Consequences of Unacceptable Behavior + +Unacceptable behavior will not be tolerated and may result in consequences such as warning, blocking of access, or +permanent removal from the project. + +## Reporting Unacceptable Behavior + +If you witness or experience any behavior that is not aligned with our code of conduct, please report it immediately by +contacting the project maintainers. You can open an issue on the project's repository at [https://git.n64.cc/nosial/ncc](https://git.n64.cc/nosial/ncc). + +## Attribution + +This Code of Conduct is adapted from the Contributor Covenant, version 2.0, available at +[https://www.contributor-covenant.org/version/2/0/code_of_conduct.html](https://www.contributor-covenant.org/version/2/0/code_of_conduct.html). + +## License + +This Code of Conduct is licensed under the Creative Commons Attribution 4.0 International License. To view a copy of +this license, visit [http://creativecommons.org/licenses/by/4.0/](http://creativecommons.org/licenses/by/4.0/). diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..2cd7c78 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,25 @@ +# Contributing to NCC + +We welcome contributions to NCC! If you have an idea for how to improve the project, please don't hesitate to reach out. +There are many ways to contribute, and we appreciate all forms of support. + +## How to Contribute + +- **Report a bug**: If you think you have found a bug in NCC, please open an issue and include as much detail as + possible to help us reproduce and fix the issue. +- **Request a feature**: Have an idea for a new feature or improvement? Open an issue to start a discussion. +- **Submit a pull request**: If you have developed a fix for a bug or have implemented a new feature, please submit a + pull request for review. + +## Code of Conduct + +We are committed to maintaining a welcoming and inclusive environment for all contributors. Please read and follow our +[Code of Conduct](https://git.n64.cc/nosial/ncc/CODE_OF_CONDUCT.md). + +## License + +By contributing to NCC, you agree that your contributions will be licensed under the MIT License. + +## Attribution + +NCC is Copyright (c) Nosial. Please include the copyright notice in any distributed code. diff --git a/DOCUMENTATION.md b/DOCUMENTATION.md index 629dfaa..31793f0 100644 --- a/DOCUMENTATION.md +++ b/DOCUMENTATION.md @@ -5,30 +5,839 @@ NCC, from basic installation, basic usage, standards and much more. ## Table of contents - - Introduction - - What is NCC? - - Advantages over other software - - ------------------------------------------------------------------------------------ + +* [NCC Documentation](#ncc-documentation) + * [Table of contents](#table-of-contents) + * [Introduction](#introduction) + * [What is NCC?](#what-is-ncc) +* [Building NCC from source](#building-ncc-from-source) + * [Requirements to build](#requirements-to-build) + * [Installing phpab](#installing-phpab) + * [Building NCC](#building-ncc) + * [Redist](#redist) + * [Tar](#tar) +* [Installing NCC](#installing-ncc) + * [Command line arguments](#command-line-arguments) +* [Uninstalling NCC](#uninstalling-ncc) +* [NCC Command-line Interface](#ncc-command-line-interface) + * [Management Commands](#management-commands) + * [Utility Commands](#utility-commands) + * [Options](#options) +* [Projects](#projects) + * [Creating a project](#creating-a-project) + * [project.json structure](#projectjson-structure) + * [project](#project) + * [project.compiler](#projectcompiler) + * [project.update_source](#projectupdatesource) + * [project.update_source.repository](#projectupdatesourcerepository) + * [assembly](#assembly) + * [execution_policies](#executionpolicies) + * [execution_policy](#executionpolicy) + * [execution_policy.execute](#executionpolicyexecute) + * [execution_policy.exit_handlers](#executionpolicyexithandlers) + * [exit_handler](#exithandler) + * [installer](#installer) + * [build](#build) + * [dependency](#dependency) + * [Source Types](#source-types) +* [Execution Policies](#execution-policies) + * [Supported Runners](#supported-runners) + * [Configuring Runners](#configuring-runners) +* [Remote Sources](#remote-sources) + * [Supported sources](#supported-sources) + * [Default sources](#default-sources) + * [Managing sources](#managing-sources) + * [Adding a source](#adding-a-source) + * [Removing a source](#removing-a-source) + * [Listing sources](#listing-sources) + * [Credential Management](#credential-management) + * [Adding credentials](#adding-credentials) + * [Removing credentials](#removing-credentials) + * [Listing credentials](#listing-credentials) +* [UUIDs](#uuids) +* [Versioning](#versioning) + * [Version Format](#version-format) + * [Version Format Compatibility](#version-format-compatibility) +* [Naming a package](#naming-a-package) + * [Naming conventions](#naming-conventions) + * [References](#references) +* [Error Codes](#error-codes) + +## Introduction -# Introduction (May 24, 2022) - -This section serves the basic introduction of NCC, what it's used for and how you can -use it in your own projects or use it to run and build other projects that are designed -to be used with NCC. +This section serves the basic introduction of NCC, what it's used for and how you can use it in your own projects or use +it to run and build other projects that are designed to be used with NCC. ## What is NCC? -NCC (*Acronym for **N**osial **C**ode **C**ompiler*) is a multi-purpose compiler, -package manager and toolkit. Allowing projects to be managed and built more easily -without having to mess with all the traditional tools that comes with your language -of choice. Right now NCC only supports PHP as it's written in PHP but extensions -for other languages/frameworks can be built into the software in the future when -the need comes for it. +NCC (*Acronym for **N**osial **C**ode **C**ompiler*) is a multi-purpose compiler, package manager and toolkit. Allowing +projects to be managed and built more easily without having to mess with all the traditional tools that comes with your +language of choice. Right now NCC only supports PHP as it's written in PHP but extensions for other languages/frameworks +can be built into the software in the future when the need comes for it. -NCC can make the process of building your code into a redistributable package much -more efficient by treating each building block of your project as a component that -is interconnected in your environment instead of the more popular route taken by -package/dependency managers such as [composer](https://getcomposer.org/), -[npm](https://www.npmjs.com/) or [pypi (or pip)](https://pypi.org/). \ No newline at end of file +NCC can make the process of building your code into a redistributable package much more efficient by treating each +building block of your project as a component that is interconnected in your environment instead of the more popular +route taken by package/dependency managers such as [composer](https://getcomposer.org/),[npm](https://www.npmjs.com/) or +[pypi (or pip)](https://pypi.org/). + + +------------------------------------------------------------------------------------ + +# Building NCC from source + +Building NCC from source is easy with very few requirements to start building. At the moment ncc can only be debugged or +tested by building a redistributable source and installing it. + +## Requirements to build + +- php8.0+ +- php-mbstring +- php-ctype +- php-common (covers tokenizer & posix among others) +- make +- phpab +- tar *(optional)* + +## Installing phpab + +phpab is also known as [PHP Autoload Builder](https://github.com/theseer/Autoload), phpab is an open source tool used +for creating autoload files, ncc needs this tool in order to generate it's autoload files whenever there's any changes +to its source code. + +This tool is only required for building and or creating a redistributable package of ncc. This component is not +required to be installed to use ncc. + +for some components that require static loading, ncc will automatically load it using its own +[autoloader](src/autoload/autoload.php) + +The recommended way to install phpab is by using [phive](https://phar.io/), if you don't have phive installed you can +install it by running these commands in your terminal (from the official documentation) + +```shell +wget -O phive.phar https://phar.io/releases/phive.phar +wget -O phive.phar.asc https://phar.io/releases/phive.phar.asc +gpg --keyserver hkps://keys.openpgp.org --recv-keys 0x9D8A98B29B2D5D79 +gpg --verify phive.phar.asc phive.phar +chmod +x phive.phar +sudo mv phive.phar /usr/local/bin/phive +``` + +Once phive is installed, you can run the final command to install phpab + +```shell +sudo phive install phpab --global +``` + +or you can run this command to install it locally + +```shell +phive install phpab +``` + +**Note:** Optionally, you may want to have `phab` available in your `$PATH`, this can be done with this command. +*(Replace `x.xx.x` with your version number)* this is if you installed it locally + +```shell +ln -s /home/user/.phive/phars/phpab-x.xx.x.phar /usr/local/bin/phpab +``` + +## Building NCC + +First, navigate to the main directory of NCC's source code where the [Makefile](Makefile) is present. If you +already attempted to or had built ncc before, it's recommended to use `make clean` before building. + +### Redist + +Running `redist` from the Makefile will generate all the required autoloader for ncc and move all the required files +into one redistributable source folder under a directory called `build/src` + +```shell +make redist +``` + + +### Tar + +Running `tar` will run redist before packaging the redistributable source into a tar.gz file that can be distributed to +other machines, this process is not a requirement. + +```shell +make tar +``` + +Once you have a populated `build/src` folder, you can simply run execute the `installer` file to install your build of +ncc onto the running machine. + +------------------------------------------------------------------------------------ + +# Installing NCC + +Installing NCC is easy, you can either download the redistributable source from the [releases](https://git.n64.cc/nosial/ncc/-/releases) +page or you can build it from source using the instructions above. + +Once you have the redistributable source, you can simply run execute the `INSTALL` file to install ncc onto the running +machine. + +## Command line arguments + +The installer accepts a few command line arguments that can be used to customize the installation process. + +`--help` Displays the help message + +`--auto` Automatically installs ncc without asking for user input. + +**Note:** To install composer along with ncc, you must also provide the `--install-composer` argument. + +`--install-composer` Installs composer along with ncc. By default, ncc will not install composer and during the +installation process you will be asked if you want to install composer along-side ncc, this will not conflict +with any existing composer installation. + +`--install-dir` Specifies the directory where ncc will be installed to. By default, ncc will be installed to `/etc/ncc` + +`--bypass-cli-check` Bypasses the check in the installer that checks if the installer is being run from the command +line, this is useful if you want to install ncc from a script. + +`--bypass-checksum` Bypasses the checksum check in the installer, this is useful if you made modifications to the +installation files and want to install a modified version of ncc. + +But this isn't recommended and the proper way to do this is to modify the source code and build ncc from source, +the Makefile task will automatically rebuild the checksum file for you. + + +------------------------------------------------------------------------------------ + +# Uninstalling NCC + +Uninstalling NCC is easy, simply delete the directory where ncc was installed to, by default this is `/etc/ncc`. + +It's recommended to run `ncc package --uninstall-all` before uninstalling ncc, this will uninstall all the packages +that were installed using ncc and remove any artifacts that were created by these packages. + +**Note:** + - To delete all the data that ncc has created, you can also delete the `/var/ncc` directory. + - Finally, remove the symlink that was created in `/usr/local/bin`to the `ncc` entry point file. + +------------------------------------------------------------------------------------ + +# NCC Command-line Interface + +NCC provides a command-line interface that can be used to manage packages, create projects, compile source code, manage +remote sources, configure ncc, and more. You can run `ncc --help` to see a list of all the available commands. + +![ncc cli](assets/ncc_cli.png) + +## Management Commands + +Management commands are used to manage ncc's configuration, remote sources, and packages. + + +`project` Manage or create a project (*see [Projects](#projects) section*) + +`package` Manage packages + +`source` Manage remote sources + +`config` Manage ncc's configuration + +## Utility Commands + +Utility commands are used to perform tasks in the current directory or project. + +`build` Compile source code of the project + +`exec` Executes a package's entry point file (package must be installed) + +## Options + +NCC also accepts a few command line arguments that can be used to alter the behavior of the command-line interface. + +`-l , --log-level ` Sets the log level, this can be one of `debug`, `verbose`, `info`, `warn`, `error`, `fatal` + +`-v, --version` Displays the version of ncc + +`-h, --help` Displays the help message + +`--basic-ascii` Renders some messages using basic ASCII characters instead of unicode characters + +`--no-color` Disables colored output + +`--no-banner` Omits displaying the NCC graphical banner + +------------------------------------------------------------------------------------ + +# Projects + +A project is a directory that contains all the source files to your program, it's similar to a workspace in other IDEs. +Usually contains a `project.json` file which contains all the information about the project that ncc needs to know. + +This can include the name of the program, the version of the program, the author of the program, the dependencies of the +program, build configurations, and more. + +This section will cover the basics of creating a project and managing it and the technical details of the `project.json` +file. + + +## Creating a project + +This is the first step in using ncc, you must create a project before you can do anything else (*not really because you +can install packages without needing to create a project and run them directly, but you get the point*) + +The NCC command-line tool provides a management command called `project` that can be used to create a new project +or to manage an existing project. + +```shell +ncc project create --package "com.example.program" --name "Example Program" +``` + +This command will create a new project in the current directory, the `--package` argument specifies the package name of +the project, this is used to identify the project and to avoid conflicts with other projects that may have the same name. + +The `--name` argument specifies the name of the project, this is used to display the name of the project in the project +manager and in the project settings. This doesn't have to be the same as the package name or unique. + +**Note:** If the options are not provided, the command will prompt you for the package name and the project name. + +For more information about the project command, you can run `ncc project --help` to display the help message. + +## project.json structure + +The `project.json` file is a JSON file that contains all the information about the project. + +When a project is created, the `project.json` file is automatically created and populated with the default values, you +can modify this file to change the default values or to add more information about the project. + +This section will go over the structure of the `project.json` file and what each field does. + +### project + +The `project` field contains information about the project, such as what compiler extension to use, options to pass on +to the compiler, and more. + +| Name | Type | Required | Description | +|---------------|--------------------------------------|----------|----------------------------------------------------------------------------------------------------| +| compiler | [project.compiler](#projectcompiler) | Yes | The compiler extension that the project uses to compile the program | +| options | `array` | No | An array of options to pass on to the compiler, the options vary depending on the compiler and NCC | +| update_source | `project.update_source` | No | The source for where the program can fetch updates from | + +### project.compiler + +The `project.compiler` field contains information about the compiler extension that the project uses to compile +the program. + +| Name | Type | Required | Description | +|-----------------|----------|----------|------------------------------------------------------------------------------------------------| +| extension | `string` | Yes | The name of the compiler extension that the project uses to compile the program | +| minimum_version | `string` | No | The minimum version of the compiler extension that the project requires to compile the program | +| maximum_version | `string` | No | The maximum version of the compiler extension that the project requires to compile the program | + +### project.update_source + +The `project.update_source` field contains information about the source where the program can fetch updates from. + +| Name | Type | Required | Description | +|------------|------------------------------------|----------|-----------------------------------------------------------------------------------------------------------------------| +| source | `string` | Yes | The source where the program can fetch updates from, see [Remote Sources](#remote-sources) for additional information | +| repository | `project.update_source.repository` | Yes | The source to configure in NCC when installing the package | + +### project.update_source.repository + +The `project.update_source.repository` field contains information about the source to configure in NCC when installing +the package. This allows you to set up a remote source that your package can use to fetch updates from, this is useful +if you want to distribute your program to other people. + +It would be useful to read more about [Remote Sources](#remote-sources) before continuing. + +| Name | Type | Required | Description | +|------|----------|----------|---------------------------------------------------------------------------------------| +| name | `string` | Yes | The name of the source to configure in NCC when installing the package (eg; `github`) | +| type | `string` | Yes | The API type to use with this source, see [Supported sources](#supported-sources) | +| host | `string` | Yes | The host of the source, this is the domain name of the source (eg; `api.github.com`) | +| ssl | `bool` | No | Whether to use SSL or not when connecting to this source | + + +### assembly + +The `assembly` field contains metadata about the program, such as the name, version, description, so on. + +| Name | Type | Required | Description | +|-------------|----------|----------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| name | `string` | Yes | The name of the package, this is used to display the name of the package (eg; `Example Program`) | +| package | `string` | Yes | The package name of the program, this is used to identify the package and to avoid conflicts with other packages that may have the same name, see [Naming a package](#naming-a-package) for additional information | +| description | `string` | No | The description of the package, this is used to display a description of the package when installing | +| company | `string` | No | The company that created the package, this is used to display the company that created the package when installing | +| product | `string` | No | The product that the package is a part of, this is used to display the product that the package is a part of when installing | +| copyright | `string` | No | The copyright of the package | +| trademark | `string` | No | The trademark of the package | +| version | `string` | Yes | The version of the package, see [Versioning](#versioning) for additional information | +| uuid | `string` | Yes | The UUID of the package, see [UUIDs](#uuids) for additional information | + +### execution_policies + +The `execution_policies` field contains information about the execution policies that the program uses, such as +the execution policy that the program uses to run additional programs during different stages of the installation +process of the package or used as the main execution policy for the program. + +Note that this field is an array of `execution_policy` objects, see [execution_policy](#executionpolicy) for additional +information. + +For more information about execution policies, see [Execution Policies](#execution-policies). + +#### execution_policy + +The `execution_policy` object contains information about the execution policy. + +| Name | Type | Required | Description | +|---------------|----------------------------------|----------|----------------------------------------------------------------------------------------------------| +| name | `string` | Yes | The name of the execution policy, this is used to identify the execution policy | +| runner | `string` | Yes | The name of the runner that the execution policy uses, see [Supported runners](#supported-runners) | +| message | `string` | No | The message to display when the execution policy is being run | +| execute | `execution_policy.execute` | Yes | The execution policy to run when the execution policy is being run | +| exit_handlers | `execution_policy.exit_handlers` | No | The exit handlers to run when the execution policy has finished running | + +#### execution_policy.execute + +The `execution_policy.execute` object contains information about how to run the execution policy when triggered. + +| Name | Type | Required | Description | +|-----------------------|------------|----------|-------------------------------------------------------------------------------------------------------------------------------------------------------| +| target | `string` | Yes | The target file to run when the execution policy is triggered, file path is relative to the location of your project.json file (eg; scripts/main.php) | +| working_directory | `string` | No | The working directory to run the process in. If not specified, the working directory will be your current working directory. | +| options | `string[]` | No | The options to pass to the process when running it. (eg; ["--foo", "bar", "-f"]) | +| environment_variables | `string[]` | No | The environment variables to pass to the process when running it. (eg; ["FOO=bar"]) | +| silent | `bool` | No | Whether to run the process silently or not. If not specified, the process will not be run silently. | +| tty | `bool` | No | Whether to run the process in a TTY or not. If not specified, the process will not be run in a TTY. | +| timeout | `int` | No | The timeout of the process in seconds. If not specified, the process will not have a timeout. | +| idle_timeout | `int` | No | The idle timeout of the process in seconds. If not specified, the process will not have an idle timeout. | + +#### execution_policy.exit_handlers + +The `execution_policy.exit_handlers` object contains information about how to run the exit handlers when the execution +policy has finished running. This is useful for running additional policies when the process has exited in a specific +way. + +The two handlers that can be executed automatically despite the exit code are `success` and `error`. Which means if the +process exits with a success exit code, the `success` handler will be run, and if the process exits with an error exit +code, the `error` handler will be run. The `warning` handler will only be run if the process exits with specified exit +code. + +| Name | Type | Required | Description | +|---------|----------------|----------|-------------------------------------------------------------------------------| +| success | `exit_handler` | No | The exit handler to run when the process has exited successfully. | +| warning | `exit_handler` | No | The exit handler to run when the process has exited with a warning exit code. | +| error | `exit_handler` | No | The exit handler to run when the process has exited with an error exit code. | + +#### exit_handler + +The `exit_handler` object contains information about how to run the exit handler when the execution policy has finished +running. + +| Name | Type | Required | Description | +|---------------|----------|----------|---------------------------------------------------------------------------------------------------------------------------------------------| +| message | `string` | No | The message to display when the exit handler is triggered | +| end_execution | `bool` | No | Whether to end the execution of the program or not if this exit handler is triggered. If not specified, the program will not end execution. | +| run | `string` | No | The name of the execution policy to run when this exit handler is triggered. | +| exit_code | `int` | No | The exit code that the process must have exited with for this exit handler to be triggered. | + +### installer + +The `installer` field contains allows you to configure the execution of policies during different stages of the +installation process of the package. Note that these files only accepts an array of strings, which are the names of +the execution policies that you want to run during the specified stage. NCC will reject the package if the execution +policy does not exist. + +| Name | Type | Required | Description | +|----------------|------------|----------|-------------------------------------------------------------------------| +| pre_install | `string[]` | No | The execution policies to run before the installation of the package. | +| post_install | `string[]` | No | The execution policies to run after the installation of the package. | +| pre_uninstall | `string[]` | No | The execution policies to run before the uninstallation of the package. | +| post_uninstall | `string[]` | No | The execution policies to run after the uninstallation of the package. | +| pre_update | `string[]` | No | The execution policies to run before the update of the package. | +| post_update | `string[]` | No | The execution policies to run after the update of the package. | + + +### build + +The `build` field contains the configuration for the build process of the package. This field is required and must be +configured correctly for the package to be built successfully. + +| Name | Type | Required | Description | +|-----------------------|-------------------------|----------|----------------------------------------------------------------------------------------------------------------------------| +| source_path | `string` | Yes | The path to the source directory of the package. (eg; src) | +| default_configuration | `string` | Yes | The default build configuration to use when building the package. | +| exclude_files | `string[]` | No | The files to exclude from the build process. | +| options | `string[]` | No | The options to pass to the build process. | +| main | `string` | No | The main execution policy to run when the package is executed, this is like the main entry point of the package. | +| define_constants | `string[]` | No | Environment constants to define during the execution of your program, these values can be accessed by the NCC Runtime API. | +| pre_build | `string[]` | No | The execution policies to run before the build process. | +| post_build | `string[]` | No | The execution policies to run after the build process. | +| dependencies | `dependency[]` | No | The dependencies that the package requires | +| configurations | `build_configuration[]` | No | Predefined build configurations that can be used to produce different builds of the package | + +### dependency + +The `dependency` object contains information about a dependency that the package requires. + +| Name | Type | Required | Description | +|-------------|----------|----------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| name | `string` | Yes | The package name of the dependency (eg; com.example.package) | +| source_type | `string` | No | Where NCC should get the dependency from, accepted values are `static`, `local` or `remote`. If not specified, NCC will assume `remote`. | +| source | `string` | No | The source of the dependency, this can a remote source (see [Remote Sources](#remote-sources)) if the source is `remote` or a a local file path if the source is `static` | +| version | `string` | No | The version of the dependency to use, if not specified, NCC will use the latest version of the dependency. (eg; 'latest') | + +#### Source Types + +Dependency source types are used to specify where NCC should get the dependency from, these are: + +- `static` - This source type is used to specify that the dependency is a local file path, this is useful for dependencies +that are not available on the remote package repository or to bundle dependencies with the package. You can only link to +pre-compiled .ncc packages, otherwise NCC will fail to install the package. +- `local` - This source type is used to specify that the dependency is a local package that is already installed on the +system. This is useful for dependencies that are already installed on the system, and you want to use them in your package +but doesn't necessarily need to pull them from a remote repository or local path. NCC expects the package to be installed +otherwise installing the package will fail unless `--skip-dependencies` is specified. +- `remote` - This source type is used to specify that the dependency is a remote package that is available on the remote +repository. This is the recommended source type to use for dependencies that are available on the remote repository. + +------------------------------------------------------------------------------------ + +# Execution Policies + +Execution policies are the policies that are used to run additional programs during different stages of the installation +or execution of the package. These policies are defined in your project.json `execution_policies` field with unique +names for each policy, so that these policies can be referenced by other policies or by NCC if configured to do so. + +## Supported Runners + +At the moment, NCC only supports a select few "runners" that can be used to run the policies, these runners are: + +- `php` - This runner is used to run PHP scripts, it is the default runner for NCC +- `bash` - This runner is used to run bash scripts +- `python` - This runner is used to run python scripts +- `python2` - This runner is used to run python2 scripts +- `python3` - This runner is used to run python3 scripts +- `perl` - This runner is used to run perl scripts +- `lua` - This runner is used to run lua scripts + + > Note: these runners are not installed by default, you will need to install them yourself. + +## Configuring Runners + +If for some reason NCC cannot automatically detect the runner that you want to use, you can configure the runner yourself +by modifying your configuration file. The configuration file is located at `/var/ncc/ncc.yaml` under the `runners` field. + +Or you can modify the configuration file by running the following command: + +```bash +ncc config -p runners.bash -v /usr/bin/bash +``` + +This will set the `bash` runner to use the `/usr/bin/bash` binary. + + > **Note:** You must have root permissions to modify the configuration file. + +------------------------------------------------------------------------------------ + +# Remote Sources + +Remote Sources are the locations where packages can be downloaded from, they are similar to repositories in other package +managers. They follow a simple syntax that allows you to specify the type of source, the location of the source, and more. + +Examples of sources are: + +- `symfony/process=latest@composer` - This is a package from the `symfony/process` package from the `composer` source +- `nosial/libs.config=latest@n64` - This is a package from the `nosial/libs.config` package from the `git.n64.cc` source + +A full example syntax may look like this: + +``` +/:=@ +``` + +This syntax is used to specify a package from a source, the syntax is split into 4 parts: + +- The vendor of the package +- The name of the package +- The branch of the package (optional) +- The version of the package (optional) +- The name of the source (needs to be configured in ncc) + +## Supported sources + +NCC supports the following sources: + +- `github` - This source uses the GitHub API to fetch packages from GitHub (Included in the default sources) +- `gitlab` - This source uses the GitLab API to fetch packages from GitLab (Can be used with self-hosted GitLab instances) + +Additional support for other sources will be added in the future. + +## Default sources + +NCC comes with a few default sources that are configured by default, these sources are: + +- packagist.org (`composer`) **Note:** This is an internal source that uses `composer` to fetch packages from packagist.org. + this is not configurable by the user. +- api.github.com (`github`) +- gitlab.com (`gitlab`) +- git.n64.cc (`n64`) +- gitgud.io (`gitgud`) + +Additional sources can be added by the user. See [Adding a source](#adding-a-source) for more information. + +## Managing sources + +You can manage sources using the `source` command in the ncc command-line tool. This command can be used to add, remove, +and list sources. For more information about the `source` command, you can run `ncc source --help` to display the help +message. + +### Adding a source + +To add a source, you can use the `add` command in the ncc `source` command-line tool. + +```shell +ncc source add --name "github" --type "github" --host "github.com" --ssl +``` + +This command will add a new source called `github` with the type `github` and the host `github.com`, the `--ssl` option +will tell ncc to use HTTPS instead of HTTP when fetching packages from this source. + +The reason to specify the type of source is to tell ncc what API to use when fetching packages from this source, for +example if you specify the type as `github` then ncc will use the GitHub API to fetch packages from this source so it's +important to specify the correct type when adding a source. + +> **Note:** You need root permissions to add a source + + +### Removing a source + +To remove a source, you can use the `remove` command in the ncc `source` command-line tool. + +```shell +ncc source remove --name "github" +``` + +> **Note:** You need root permissions to remove a source + +> **Note:** Removing a source also removes the ability for some packages to be fetched or updated from this source + + +### Listing sources + +To list all the sources, you can use the `list` command in the ncc `source` command-line tool. + +```shell +ncc source list +``` + +## Credential Management + +Some sources require credentials to be able to fetch packages from them, for example the `gitlab` source requires +credentials to be able to fetch packages from a self-hosted GitLab instance. NCC supports storing credentials for +sources in a secure way using the `cred` command in the ncc command-line tool. + +### Adding credentials + +To add credentials for a source, you can use the `add` command in the ncc `cred` command-line tool. + +```shell +ncc cred add --alias "My Alias" --auth-type login --username "myusername" --password "mypassword" +``` + +To add a private access token as a credential, you can specify the `--auth-type` as `pat` and specify the token as +`--token` instead of providing `--username` and `--password`. + +```shell +ncc cred add --alias "My Alias" --auth-type pat --token="mytoken" +``` + +By default, ncc will encrypt the entry except for the alias using the password/token that you provide. + +However, because it's encrypted you will need to provide the password/token when using the credential since ncc will +not be able to decrypt the entry without a password. To avoid being asked for the password/token every time you use the +credential, you can pass on the `--no-encryption` option to the `cred` command-line tool. + +```shell +ncc cred add --alias "My Alias" --auth-type login --username "myusername" --password "mypassword" --no-encryption +``` + +Encryption is applied individually to each credential, so you can have some credentials encrypted and some not encrypted. + +> **Note:** You need root permissions to add credentials + + +### Removing credentials + +To remove credentials, you can use the `remove` command in the ncc `cred` command-line tool. + +```shell +ncc cred remove --alias "My Alias" +``` + +> **Note:** You need root permissions to remove credentials + + +### Listing credentials + +To list all the credentials, you can use the `list` command in the ncc `cred` command-line tool. this will return +a list of all the credentials that are stored in the credential store with additional information about each entry. + +```shell +ncc cred list +``` + +------------------------------------------------------------------------------------ + +# UUIDs + +UUIDs are used to uniquely identify a package, at the moment ncc doesn't do anything meaningful with UUIDs but in the +future it will be used to identify packages and to prevent conflicts between packages with the same name. + +The standard UUID format used is version 1, which is a time-based UUID. This means that the UUID is generated using +the current time and the MAC address of the computer that generated the UUID. + +`````` +xxxxxxxx-xxxx-1xxx-yxxx-xxxxxxxxxxxx +`````` + +UUIDs are automatically generated when a package is created, you can also manually specify a UUID by editing the +`project.json` file in the project directory, this field is found under `assembly.uuid`, see [assembly](#assembly) for +more information. + +> **Note:** Invalid UUIDs will cause the package to be rejected by ncc + +------------------------------------------------------------------------------------ + +# Versioning + +NCC uses a standard versioning system, this system is based on the [Semantic Versioning](https://semver.org/) system. + +## Version Format + +The version format is as follows: + +`````` +MAJOR.MINOR.PATCH +`````` + +- `MAJOR` is the major version of the package, this version is incremented when a major change is made to the package +- `MINOR` is the minor version of the package, this version is incremented when a minor change is made to the package +- `PATCH` is the patch version of the package, this version is incremented when a patch is made to the package + + +## Version Format Compatibility + +NCC will attempt to convert non-compatible versions to a compatible version when it comes to installing packages that +isn't built for ncc. + +> **Note:** NCC will reject packages with invalid version numbers, sometimes this can happen when the compatibility layer +fails or when the version number is invalid. + +------------------------------------------------------------------------------------ + +# Naming a package + +NCC Follows the same naming convention as Java's naming convention. The purpose of naming a package this way is +to easily create a "Name" of the package, this string of information contains + +- The developer/organization behind the package +- The package name itself + + +## Naming conventions + +Package names are written in all lower-case due to the fact that some operating systems treats file names +differently, for example on Linux `Aa.txt` and `aa.txt`are two entirely different file names because of the +capitalization and on Windows it's treated as the same file name. + +Organizations or small developers use their domain name in reverse to begin their package names, for example +`net.nosial.example` is a package named `example` created by a programmer at `nosial.net` + +Just like the Java naming convention, to avoid conflicts of the same package name developers can use something +different, for example as pointed out in Java's package naming convention developers can instead use something +like a region to name packages, for example `net.nosial.region.example` + + +## References + +For Java's package naming conventions see [Naming a Package](https://docs.oracle.com/javase/tutorial/java/package/namingpkgs.html) +from the Oracle's Java documentation resource, as the same rules apply to NCC except for *some* illegal naming +conventions such as packages not being able to begin with `int` or numbers + +------------------------------------------------------------------------------------ + +# Error Codes + +NCC uses error codes to identify errors, these error codes are used to identify errors in the ncc command-line tool +and in the ncc API. + +| Error Code | Name | +|:----------:|:--------------------------------------| +| `-1700` | InvalidProjectConfigurationException | +| `-1701` | FileNotFoundException | +| `-1702` | DirectoryNotFoundException | +| `-1703` | InvalidScopeException | +| `-1704` | AccessDeniedException | +| `-1705` | MalformedJsonException | +| `-1706` | RuntimeException | +| `-1707` | InvalidCredentialsEntryException | +| `-1708` | ComponentVersionNotFoundException | +| `-1709` | ConstantReadonlyException | +| `-1710` | InvalidPackageNameException | +| `-1711` | InvalidVersionNumberException | +| `-1712` | InvalidProjectNameException | +| `-1713` | ProjectAlreadyExistsException | +| `-1714` | AutoloadGeneratorException | +| `-1715` | NoUnitsFoundException | +| `-1716` | UnsupportedPackageException | +| `-1717` | NotImplementedException | +| `-1718` | InvalidPackageException | +| `-1719` | InvalidConstantNameException | +| `-1720` | PackagePreparationFailedException | +| `-1721` | BuildConfigurationNotFoundException | +| `-1722` | InvalidProjectBuildConfiguration | +| `-1723` | UnsupportedCompilerExtensionException | +| `-1724` | InvalidPropertyValueException | +| `-1725` | InvalidVersionConfigurationException | +| `-1726` | UnsupportedExtensionVersionException | +| `-1727` | BuildException | +| `-1728` | PackageParsingException | +| `-1729` | PackageLockException | +| `-1730` | InstallationException | +| `-1731` | UnsupportedComponentTypeException | +| `-1732` | ComponentDecodeException | +| `-1733` | ComponentChecksumException | +| `-1734` | ResourceChecksumException | +| `-1735` | IOException | +| `-1736` | UnsupportedRunnerException | +| `-1737` | VersionNotFoundException | +| `-1738` | UndefinedExecutionPolicyException | +| `-1739` | InvalidExecutionPolicyName | +| `-1740` | ProjectConfigurationNotFoundException | +| `-1741` | RunnerExecutionException | +| `-1742` | NoAvailableUnitsException | +| `-1743` | ExecutionUnitNotFoundException | +| `-1744` | PackageAlreadyInstalledException | +| `-1745` | PackageNotFoundException | +| `-1746` | ComposerDisabledException | +| `-1747` | InternalComposerNotAvailable | +| `-1748` | ComposerNotAvailableException | +| `-1749` | ComposerException | +| `-1750` | UserAbortedOperationException | +| `-1751` | MissingDependencyException | +| `-1752` | HttpException | +| `-1753` | UnsupportedRemoteSourceTypeException | +| `-1754` | GitCloneException | +| `-1755` | GitCheckoutException | +| `-1756` | GitlabServiceException | +| `-1757` | ImportException | +| `-1758` | GitTagsException | +| `-1759` | GithubServiceException | +| `-1760` | AuthenticationException | +| `-1761` | NotSupportedException | +| `-1762` | UnsupportedProjectTypeException | +| `-1763` | UnsupportedArchiveException | +| `-1764` | ArchiveException | +| `-1765` | PackageFetchException | +| `-1766` | InvalidBuildConfigurationException | +| `-1767` | InvalidDependencyConfiguration | +| `-1768` | SymlinkException | diff --git a/LICENSE b/LICENSE index 3fad6f1..723b74e 100644 --- a/LICENSE +++ b/LICENSE @@ -1,355 +1,14 @@ ------------------------- -Nosial - NCC +Copyright 2022-2023 Nosial - All Rights Reserved. -Copyright (C) 2022-2022. Nosial - All Rights Reserved. +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the Software without restriction, including without limitation the +rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to the following conditions: -Unauthorized copying of this file, via any medium is strictly prohibited -Proprietary and confidential, written by Zi Xing Narrakas +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the +Software. ------------------------- -Symfony - Process - -Copyright (c) 2004-2022 Fabien Potencier - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is furnished -to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - ------------------------- -defuse - php-encryption - -The MIT License (MIT) - -Copyright (c) 2016 Taylor Hornby -Copyright (c) 2016 Paragon Initiative Enterprises . - -Permission is hereby granted, free of charge, to any person obtaining a copy of -this software and associated documentation files (the "Software"), to deal in -the Software without restriction, including without limitation the rights to -use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of -the Software, and to permit persons to whom the Software is furnished to do so, -subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS -FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR -COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER -IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - ------------------------- -Symfony - uid - -Copyright (c) 2020-2022 Fabien Potencier - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is furnished -to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - - ------------------------- -Symfony - polyfill-mbstring - -Copyright (c) 2015-2019 Fabien Potencier - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is furnished -to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - - ------------------------- -Symfony - polyfill-ctype - -Copyright (c) 2018-2019 Fabien Potencier - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is furnished -to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - - ------------------------- -Symfony - polyfill-uuid - -Copyright (c) 2018-2019 Fabien Potencier - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is furnished -to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - - - ------------------------- -dealnews.com, Inc. - Inline ProgressBar CLI - -Copyright (c) 2010, dealnews.com, Inc. All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: - - * Redistributions of source code must retain the above copyright notice, - this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above copyright - notice, this list of conditions and the following disclaimer in the - documentation and/or other materials provided with the distribution. - * Neither the name of dealnews.com, Inc. nor the names of its contributors - may be used to endorse or promote products derived from this software - without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE -LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -POSSIBILITY OF SUCH DAMAGE. - - ------------------------- -Symfony - Filesystem - -Copyright (c) 2004-2022 Fabien Potencier - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is furnished -to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - - ------------------------- -Symfony - Yaml - -Copyright (c) 2004-2022 Fabien Potencier - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is furnished -to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - ------------------------- -theseer - Autoload - -Autoload Builder - -Copyright (c) 2010-2016 Arne Blankerts and Contributors -All rights reserved. - -Redistribution and use in source and binary forms, with or without modification, -are permitted provided that the following conditions are met: - -* Redistributions of source code must retain the above copyright notice, - this list of conditions and the following disclaimer. - -* Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - -* Neither the name of Arne Blankerts nor the names of contributors - may be used to endorse or promote products derived from this software - without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT * NOT LIMITED TO, -THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER ORCONTRIBUTORS -BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, -OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -POSSIBILITY OF SUCH DAMAGE. - - ------------------------- -theseer - DirectoryScanner - -DirectoryScanner - -Copyright (c) 2009-2014 Arne Blankerts -All rights reserved. - -Redistribution and use in source and binary forms, with or without modification, -are permitted provided that the following conditions are met: - -* Redistributions of source code must retain the above copyright notice, - this list of conditions and the following disclaimer. - -* Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - -* Neither the name of Arne Blankerts nor the names of contributors - may be used to endorse or promote products derived from this software - without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT * NOT LIMITED TO, -THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER ORCONTRIBUTORS -BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, -OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -POSSIBILITY OF SUCH DAMAGE. - - ------------------------- -nikic - PhpParser - -BSD 3-Clause License - -Copyright (c) 2011, Nikita Popov -All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: - -1. Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - -2. Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - -3. Neither the name of the copyright holder nor the names of its - contributors may be used to endorse or promote products derived from - this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE -FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - - ------------------------- -jelix - version - -Copyright (C) 2009-2016 Laurent Jouanneau - -Permission is hereby granted, free of charge, to any person obtaining a copy of -this software and associated documentation files (the "Software"), to deal in -the Software without restriction, including without limitation the rights to -use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of -the Software, and to permit persons to whom the Software is furnished to do so, -subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS -FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR -COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER -IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/Makefile b/Makefile index 6931230..f2a4382 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,5 @@ -PHPCC=/usr/bin/php -PHPAB=/usr/bin/phpab +PHPCC:=$(shell which php) +PHPAB:=$(shell which phpab) BUILD_PATH=build SRC_PATH=src @@ -80,25 +80,28 @@ $(SRC_PATH)/ncc/autoload_spl.php: $(SRC_PATH)/ncc/Objects \ $(SRC_PATH)/ncc/Runtime \ $(SRC_PATH)/ncc/Utilities \ - $(SRC_PATH)/ncc/ncc.php + $(SRC_PATH)/ncc/ncc.php \ + $(SRC_PATH)/ncc/Runtime.php redist: autoload rm -rf $(BUILD_PATH)/src mkdir -p $(BUILD_PATH)/src cp -rf $(SRC_PATH)/ncc/* $(BUILD_PATH)/src - cp $(SRC_PATH)/installer/installer $(BUILD_PATH)/$(SRC_PATH)/INSTALL - cp $(SRC_PATH)/installer/ncc.sh $(BUILD_PATH)/$(SRC_PATH)/ncc.sh - cp $(SRC_PATH)/config/ncc.yaml $(BUILD_PATH)/$(SRC_PATH)/default_config.yaml; - cp $(SRC_PATH)/config/ncc.yaml $(BUILD_PATH)/$(SRC_PATH)/CLI/template_config.yaml; - cp $(SRC_PATH)/installer/extension $(BUILD_PATH)/$(SRC_PATH)/extension - chmod +x $(BUILD_PATH)/$(SRC_PATH)/INSTALL - cp LICENSE $(BUILD_PATH)/$(SRC_PATH)/LICENSE - cp README.md $(BUILD_PATH)/$(SRC_PATH)/README.md - cp $(SRC_PATH)/installer/hash_check.php $(BUILD_PATH)/$(SRC_PATH)/hash_check.php; $(PHPCC) $(BUILD_PATH)/$(SRC_PATH)/hash_check.php; rm $(BUILD_PATH)/$(SRC_PATH)/hash_check.php - cp $(SRC_PATH)/installer/generate_build_files.php $(BUILD_PATH)/$(SRC_PATH)/generate_build_files.php; $(PHPCC) $(BUILD_PATH)/$(SRC_PATH)/generate_build_files.php; rm $(BUILD_PATH)/$(SRC_PATH)/generate_build_files.php + cp $(SRC_PATH)/installer/installer $(BUILD_PATH)/src/INSTALL + cp $(SRC_PATH)/installer/ncc.sh $(BUILD_PATH)/src/ncc.sh + cp $(SRC_PATH)/config/ncc.yaml $(BUILD_PATH)/src/default_config.yaml; + cp $(SRC_PATH)/config/ncc.yaml $(BUILD_PATH)/src/CLI/template_config.yaml; + cp $(SRC_PATH)/installer/extension $(BUILD_PATH)/src/extension + chmod +x $(BUILD_PATH)/src/INSTALL + cp LICENSE $(BUILD_PATH)/src/LICENSE + cp README.md $(BUILD_PATH)/src/README.md + cp $(SRC_PATH)/installer/hash_check.php $(BUILD_PATH)/src/hash_check.php; $(PHPCC) $(BUILD_PATH)/src/hash_check.php; rm $(BUILD_PATH)/src/hash_check.php + cp $(SRC_PATH)/installer/generate_build_files.php $(BUILD_PATH)/src/generate_build_files.php; $(PHPCC) $(BUILD_PATH)/src/generate_build_files.php; rm $(BUILD_PATH)/src/generate_build_files.php + mkdir -p $(BUILD_PATH)/src/repositories + cp -rf $(SRC_PATH)/default_repositories/*.json $(BUILD_PATH)/src/repositories tar: redist - cd $(BUILD_PATH)/src; tar -czvf ../ncc.tar.gz * + cd $(BUILD_PATH)/src; tar -czvf ../build.tar.gz * clean: rm -rf $(BUILD_PATH) diff --git a/README.md b/README.md index 9db1970..83fa984 100644 --- a/README.md +++ b/README.md @@ -4,38 +4,60 @@ Nosial Code Compiler is a program written in PHP designed to be a multi-purpose This program is a complete re-write of the now defunct [PHP Package Manager (PPM)](https://git.n64.cc/intellivoid/ppm) toolkit offering more features, security and proper code licensing and copyrighting for the components used for the project. -NCC Cannot compile, read or use PPM packages (.ppm) files or work with project sources designed to be built with PPM, however -a PPM extension may be built in the future to allow for backwards compatibility. +## Alpha Stage + +NCC is currently in alpha stage, meaning that it's not fully functional and may not work on your system. If you find any bugs +or issues please report them to the [GitHub Issue Tracker](https://git.n64.cc/intellivoid/ncc/issues). + +At the moment NCC is currently being used while developing other software, this serves as a test run to +improve on changes for the next version. + +## Version History + + - 1.0.0 Alpha - Initial release ([changelog](changelog/v1.0.0_alpha.md)) + +## Repository Mirrors + +The official repository for NCC is hosted on [GitLab](https://git.n64.cc/intellivoid/ncc), however, you can also find +mirrors of the repository mirrored on different platforms, including +community powered mirrors. (more to come) + + - [git.it-kuny.ch](https://git.it-kuny.ch) + - [git.martinvlba.eu](https://git.martinvlba.eu/Nosial/ncc) -## Notes +# Contributing - > While NCC has windows compatibility in mind, not all compiler extensions or features will work correctly. NCC is - > designed to be used in production in a Unix environment and Windows should only be used for development purposes. +We welcome contributions to NCC! If you have an idea for how to improve the project, please don't hesitate to reach out. +There are many ways to contribute, and we appreciate all forms of support. - > Compiler extensions requires their own set of dependencies to be met, for example Java compilers will require JDK +For more information on how to contribute, please read the [CONTRIBUTING.md](CONTRIBUTING.md) file. - > NCC is designed to run only on a PHP 8.0+ environment, compiler extensions can have support for different PHP versions. - > Third-party dependencies and included libraries has a dedicated namespace for `ncc` to avoid user land conflicts if - > the user wishes to install and use one of the same dependencies that NCC uses. +# Code of Conduct -## Authors - - Zi Xing Narrakas (netkas) <[netkas@n64.cc](mailto:netkas@64.cc)> +We are committed to maintaining a welcoming and inclusive environment for all contributors. Please read and follow our +[Code of Conduct](CODE_OF_CONDUCT.md). -## Special Thanks - - Marc Gutt (mgutt) <[marc@gutt.it](mailto:marc@gutt.it)> - - Debusschère Alexandre ([debuss](https://github.com/debuss)) +# Authors + +- Zi Xing Narrakas (netkas) <[netkas@n64.cc](mailto:netkas@64.cc)> + + +# Special Thanks + +- Marc Gutt (mgutt) <[marc@gutt.it](mailto:marc@gutt.it)> +- Debusschère Alexandre ([debuss](https://github.com/debuss)) + + +# Copyright + +- Copyright (c) 2022-2023, Nosial - All Rights Reserved -## Copyright -- Copyright (c) 2022-2022, Nosial - All Rights Reserved -- Copyright (c) 2004-2022, Fabien Potencier -- Copyright (c) 2010, dealnews.com, Inc. All rights reserved. -- Copyright (c) 2013 Austin Hyde -- Copyright (C) 2009-2016 Laurent Jouanneau -- Copyright (c) 2011, Nikita Popov -- Copyright (c) 2010-2016 Arne Blankerts and Contributors # Licenses -Multiple licenses can be found at [LICENSE](LICENSE) \ No newline at end of file +NCC is licensed under the MIT License, see [LICENSE](LICENSE) for more information. + +Multiple licenses for the open source components used in this +project can be found at [LICENSE](LICENSES) \ No newline at end of file diff --git a/assets/ncc_cli.png b/assets/ncc_cli.png new file mode 100644 index 0000000000000000000000000000000000000000..64fb3c4ae7ce144ad7b86965e16319180e73732d GIT binary patch literal 92689 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 az5f5>$-Gt5RAYJ=fWXt$&t;ucLK6V4YqMzp literal 0 HcmV?d00001 diff --git a/changelog/v1.0.0_alpha.md b/changelog/v1.0.0_alpha.md new file mode 100644 index 0000000..814a5f0 --- /dev/null +++ b/changelog/v1.0.0_alpha.md @@ -0,0 +1,18 @@ +# v1.0.0 Alpha + +First release of NCC (Nosial Code Compiler) in alpha stage. + +## Alpha Stage + +NCC is in its alpha stage, meaning that it's not fully +functional and may not work on your system. If you find +any bugs or issues please report them to the +[Issue Tracker](https://git.n64.cc/intellivoid/ncc/issues). + +At the moment NCC is currently being used while developing +other software, this serves as a test run to improve on +changes for the next version. + +## Changelog + + - Initial release \ No newline at end of file diff --git a/docs/building_ncc.md b/docs/building_ncc.md deleted file mode 100644 index 2d3b6f6..0000000 --- a/docs/building_ncc.md +++ /dev/null @@ -1,94 +0,0 @@ -# Building NCC from source - -Building NCC from source is easy with very few requirements -to start building. At the moment ncc can only be debugged -or tested by building a redistributable source and -installing it. - -## Requirements to build - - - php8.0+ - - php-mbstring - - php-ctype - - php-tokenizer *(or php-common)* - - make - - phpab - - tar *(optional)* - -## Installing phpab - -phpab is also known as [PHP Autoload Builder](https://github.com/theseer/Autoload), -phpab is an open source tool used for creating autoload -files, ncc needs this tool in order to generate it's -autoload files whenever there's any changes to its source -code. - -This tool is only required for building and or creating a -redistributable package of ncc. This component is not -required to be installed to use ncc. - -for some components that require static loading, ncc will -automatically load it using it's own -[autoloader](../src/autoload/autoload.php) - -The recommended way to install phpab is by using [phive](https://phar.io/), -if you don't have phive installed you can install it by -running these commands in your terminal (from the official documentation) - -```shell -wget -O phive.phar https://phar.io/releases/phive.phar -wget -O phive.phar.asc https://phar.io/releases/phive.phar.asc -gpg --keyserver hkps://keys.openpgp.org --recv-keys 0x9D8A98B29B2D5D79 -gpg --verify phive.phar.asc phive.phar -chmod +x phive.phar -sudo mv phive.phar /usr/local/bin/phive -``` - -Once phive is installed, you can run the final command to -install phpab -```shell -phive install phpab -``` - -**Note:** Optionally, you may want to have `phab` available in your -`$PATH`, this can be done with this command. *(Replace `x.xx.x` with your -version number)* - -```shell -ln -s /home/user/.phive/phars/phpab-x.xx.x.phar /usr/bin/phpab -``` - -## Building NCC - -First, navigate to the main directory of NCC's source code -where the [Makefile](../Makefile) is present. If you -already attempted to or had built ncc before, it's -recommended to use `make clean` before building. - - -### Redist - -Running `redist` from the Makefile will generate all the -required autoloaders for ncc and move all the required -files into one redistributable source folder under a -directory called `build/src` - -```shell -make redist -``` - - -### Tar - -Running `tar` will run redist before packaging the -redistributable source into a tar.gz file that can -be distributed to other machines, this process is not -a requirement. - -```shell -make tar -``` - -Once you have a populated `build/src` folder, you can -simply run execute the `installer` file to install your -build of ncc onto the running machine. \ No newline at end of file diff --git a/docs/package_name.md b/docs/package_name.md deleted file mode 100644 index 0b4586e..0000000 --- a/docs/package_name.md +++ /dev/null @@ -1,43 +0,0 @@ -# Naming a package - -**Updated on Tuesday, July 26, 2022** - -NCC Follows the same naming convention as Java's naming -convention. The purpose of naming a package this way is -to easily create a "Name" of the package, this string -of information contains - - - The developer/organization behind the package - - The package name itself - - -# Naming conventions - -Package names are written in all lower-case due to the -fact that some operating systems treats file names -differently, for example on Linux `Aa.txt` and `aa.txt` -are two entirely different file names because of the -capitalization and on Windows it's treated as the same -file name. - -Organizations or small developers use their domain name -in reverse to begin their package names, for example -`net.nosial.example` is a package named `example` -created by a programmer at `nosial.net` - -Just like the Java naming convention, to avoid conflicts -of the same package name developers can use something -different, for example as pointed out in Java's package -naming convention developers can instead use something -like a region to name packages, for example -`net.nosial.region.example` - - -# References - -For Java's package naming conventions see -[Naming a Package](https://docs.oracle.com/javase/tutorial/java/package/namingpkgs.html) -from the Oracle's Java documentation resource, as the -same rules apply to NCC except for *some* illegal naming -conventions such as packages not being able to begin -with `int` or numbers \ No newline at end of file diff --git a/docs/project_configuration/execution_policy.md b/docs/project_configuration/execution_policy.md deleted file mode 100644 index f5b851b..0000000 --- a/docs/project_configuration/execution_policy.md +++ /dev/null @@ -1,151 +0,0 @@ -# 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/config/ncc.yaml b/src/config/ncc.yaml index 0b713ba..36c6488 100644 --- a/src/config/ncc.yaml +++ b/src/config/ncc.yaml @@ -19,12 +19,6 @@ ncc: php: # The main executable path for PHP that NCC should use executable_path: "/usr/bin/php" - runtime: - # Whether to initialize NCC when running `require('ncc');` - initialize_on_require: true - - # if NCC should handle fatal exceptions during execution - handle_exceptions: true git: # if git is enabled or not @@ -55,10 +49,10 @@ composer: quiet: false # Disable ANSI output - no_ansi: false + no_ansi: true # Do not ask any interactive question - no_interaction: false + no_interaction: true # Display timing and memory usage information profile: false diff --git a/src/default_repositories/custom_repositories.json b/src/default_repositories/custom_repositories.json new file mode 100644 index 0000000..aa1a562 --- /dev/null +++ b/src/default_repositories/custom_repositories.json @@ -0,0 +1,6 @@ +[ + "gitgud.json", + "github.json", + "gitlab.json", + "n64.json" +] \ No newline at end of file diff --git a/src/default_repositories/gitgud.json b/src/default_repositories/gitgud.json new file mode 100644 index 0000000..83f904e --- /dev/null +++ b/src/default_repositories/gitgud.json @@ -0,0 +1,6 @@ +{ + "name": "gitgud", + "type": "gitlab", + "host": "gitgud.io", + "ssl": true +} \ No newline at end of file diff --git a/src/default_repositories/github.json b/src/default_repositories/github.json new file mode 100644 index 0000000..f05cc8c --- /dev/null +++ b/src/default_repositories/github.json @@ -0,0 +1,6 @@ +{ + "name": "github", + "type": "github", + "host": "api.github.com", + "ssl": true +} \ No newline at end of file diff --git a/src/default_repositories/gitlab.json b/src/default_repositories/gitlab.json new file mode 100644 index 0000000..2694fbd --- /dev/null +++ b/src/default_repositories/gitlab.json @@ -0,0 +1,6 @@ +{ + "name": "gitlab", + "type": "gitlab", + "host": "gitlab.com", + "ssl": true +} \ No newline at end of file diff --git a/src/default_repositories/n64.json b/src/default_repositories/n64.json new file mode 100644 index 0000000..ac57ee8 --- /dev/null +++ b/src/default_repositories/n64.json @@ -0,0 +1,6 @@ +{ + "name": "n64", + "type": "gitlab", + "host": "git.n64.cc", + "ssl": true +} \ No newline at end of file diff --git a/src/installer/extension b/src/installer/extension index aaff950..2ccc72b 100644 --- a/src/installer/extension +++ b/src/installer/extension @@ -1,8 +1,19 @@ exists(PathFinder::getConfigurationFile())) { - $config_backup = IO::fread(PathFinder::getConfigurationFile()); + $config_backup = Yaml::parseFile(PathFinder::getConfigurationFile()); } } catch (Exception $e) @@ -736,33 +737,73 @@ // Create/Update configuration file $config_obj = Yaml::parseFile(__DIR__ . DIRECTORY_SEPARATOR . 'default_config.yaml'); - // Update the old configuration - if($config_backup !== null) + if(!function_exists('array_replace_recursive')) { - $old_config_obj = Yaml::parse($config_backup); - foreach($old_config_obj as $section => $value) + /** + * @param $array + * @param $array1 + * @return array|mixed + * @author + * @noinspection PhpMissingReturnTypeInspection + */ + function array_replace_recursive($array, $array1) { - if(isset($config_obj[$section])) + // handle the arguments, merge one by one + $args = func_get_args(); + $array = $args[0]; + if (!is_array($array)) { - foreach($value as $section_item => $section_value) + return $array; + } + for ($i = 1; $i < count($args); $i++) + { + if (is_array($args[$i])) { - if(!isset($config_obj[$section][$section_item])) - { - $config_obj[$section][$section_item] = $section_value; - } + $array = recurse($array, $args[$i]); } } - else - { - $config_obj[$section] = $value; - } + return $array; } } + if(!function_exists('recurse')) + { + /** + * @param $array + * @param $array1 + * @return mixed + * @author + * @noinspection PhpMissingReturnTypeInspection + */ + function recurse($array, $array1) + { + foreach ($array1 as $key => $value) + { + // create new key in $array, if it is empty or not an array + /** @noinspection PhpConditionAlreadyCheckedInspection */ + if (!isset($array[$key]) || (isset($array[$key]) && !is_array($array[$key]))) + { + $array[$key] = array(); + } + + // overwrite the value in the base array + if (is_array($value)) + { + $value = recurse($array[$key], $value); + } + $array[$key] = $value; + } + return $array; + } + } + + + if($config_backup !== null) + $config_obj = array_replace_recursive($config_obj, $config_backup); + if($config_backup == null) { Console::out('Generating ncc.yaml'); - } else { @@ -779,6 +820,57 @@ return; } + if($NCC_FILESYSTEM->exists(__DIR__ . DIRECTORY_SEPARATOR . 'repositories')) + { + if(!$NCC_FILESYSTEM->exists(__DIR__ . DIRECTORY_SEPARATOR . 'repositories' . DIRECTORY_SEPARATOR . 'custom_repositories.json')) + return; + + try + { + $custom_repositories = Functions::loadJsonFile(__DIR__ . DIRECTORY_SEPARATOR . 'repositories' . DIRECTORY_SEPARATOR . 'custom_repositories.json', Functions::FORCE_ARRAY); + } + catch(Exception $e) + { + $custom_repositories = null; + Console::outWarning(sprintf('Failed to load custom repositories: %s', $e->getMessage())); + } + + if($custom_repositories !== null) + { + $source_manager = new RemoteSourcesManager(); + foreach($custom_repositories as $repository) + { + $repo_path = __DIR__ . DIRECTORY_SEPARATOR . 'repositories' . DIRECTORY_SEPARATOR . $repository; + if($NCC_FILESYSTEM->exists($repo_path)) + { + try + { + $definedEntry = DefinedRemoteSource::fromArray(Functions::loadJsonFile($repo_path, Functions::FORCE_ARRAY)); + if(!$source_manager->getRemoteSource($definedEntry->Name)) + $source_manager->addRemoteSource($definedEntry); + } + catch(Exception $e) + { + Console::outWarning(sprintf('Failed to load custom repository %s: %s', $repository, $e->getMessage())); + } + } + else + { + Console::outWarning(sprintf('Failed to load custom repository %s, file does not exist', $repository)); + } + } + + try + { + $source_manager->save(); + } + catch (\ncc\Exceptions\IOException $e) + { + Console::outWarning(sprintf('Failed to save sources: %s', $e->getMessage())); + } + } + } + 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/AuthenticationSource.php b/src/ncc/Abstracts/AuthenticationSource.php deleted file mode 100644 index c5b29f8..0000000 --- a/src/ncc/Abstracts/AuthenticationSource.php +++ /dev/null @@ -1,12 +0,0 @@ -getPackageLock()->getPackage($package); + } + catch(Exception $e) + { + Console::outException('Package ' . $package . ' is not installed', $e, 1); + return; + } + + try + { + $version_entry = $package_entry->getVersion($version); + } + catch(Exception $e) + { + Console::outException('Version ' . $version . ' is not installed', $e, 1); + return; + } + + try + { + $units = $execution_pointer_manager->getUnits($package_entry->Name, $version_entry->Version); + } + catch(Exception $e) + { + Console::outException(sprintf('Cannot load execution units for package \'%s\'', $package), $e, 1); + return; + } + + if(!in_array($unit_name, $units)) + { + Console::outError(sprintf('Unit \'%s\' is not configured for package \'%s\'', $unit_name, $package), true, 1); + return; + } + + $options = []; + + if($set_args != null) + { + global $argv; + $args_index = array_search('--exec-args', $argv); + $options = array_slice($argv, $args_index + 1); + } + + try + { + exit($execution_pointer_manager->executeUnit($package_entry->Name, $version_entry->Version, $unit_name, $options)); + } + catch(Exception $e) + { + Console::outException(sprintf('Cannot execute execution point \'%s\' in package \'%s\'', $unit_name, $package), $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(['exec', '--package'], '(Required) The package to execute'), + new CliHelpSection(['--exec-version'], '(default: latest) The version of the package to execute'), + new CliHelpSection(['--exec-unit'], '(default: main) The unit point of the package to execute'), + new CliHelpSection(['--exec-args'], '(optional) Anything past this point will be passed to the execution unit'), + ]; + + $options_padding = Functions::detectParametersPadding($options) + 4; + + Console::out('Usage: ncc exec --package [options] [arguments]'); + Console::out('Options:' . PHP_EOL); + foreach($options as $option) + { + Console::out(' ' . $option->toString($options_padding)); + } + + Console::out(PHP_EOL . 'Arguments:' . PHP_EOL); + Console::out(' The arguments to pass to the program'); + Console::out(PHP_EOL . 'Example Usage:' . PHP_EOL); + Console::out(' ncc exec --package com.example.program'); + Console::out(' ncc exec --package com.example.program --exec-version 1.0.0'); + Console::out(' ncc exec --package com.example.program --exec-version 1.0.0 --exec-unit setup'); + Console::out(' ncc exec --package com.example.program --exec-args --foo --bar --extra=test'); + } + } \ No newline at end of file diff --git a/src/ncc/CLI/CredentialMenu.php b/src/ncc/CLI/CredentialMenu.php deleted file mode 100644 index a82336f..0000000 --- a/src/ncc/CLI/CredentialMenu.php +++ /dev/null @@ -1,72 +0,0 @@ -toString($options_padding) . PHP_EOL); - } - print(PHP_EOL); - } - } \ No newline at end of file diff --git a/src/ncc/CLI/HelpMenu.php b/src/ncc/CLI/HelpMenu.php index 140bae4..8381429 100644 --- a/src/ncc/CLI/HelpMenu.php +++ b/src/ncc/CLI/HelpMenu.php @@ -1,9 +1,33 @@ toString($options_padding)); - } + ]); } /** @@ -67,20 +86,14 @@ */ private static function displayManagementCommands(): void { - $commands = [ + Console::out('Management Commands:'); + Console::outHelpSections([ new CliHelpSection(['project'], 'Manages the current project'), new CliHelpSection(['package'], 'Manages the package system'), - new CliHelpSection(['cache'], 'Manages the system cache'), - new CliHelpSection(['credential'], 'Manages credentials'), + new CliHelpSection(['cred'], 'Manages credentials'), new CliHelpSection(['config'], 'Changes NCC configuration values'), - ]; - $commands_padding = \ncc\Utilities\Functions::detectParametersPadding($commands) + 2; - - Console::out('Management Commands:'); - foreach($commands as $command) - { - Console::out(' ' . $command->toString($commands_padding)); - } + new CliHelpSection(['source'], 'Manages remote sources'), + ]); } /** @@ -90,35 +103,10 @@ */ private static function displayMainCommands(): void { - $commands = [ - new CliHelpSection(['build'], 'Builds the current project'), - new CliHelpSection(['main'], 'Executes the main entrypoint of a package') - ]; - $commands_padding = \ncc\Utilities\Functions::detectParametersPadding($commands) + 2; - Console::out('Commands:'); - foreach($commands as $command) - { - Console::out(' ' . $command->toString($commands_padding)); - } - } - - /** - * Displays the main commands section - * - * @return void - */ - private static function displayExtensions(): void - { - $extensions = [ - new CliHelpSection(['exphp'], 'The PHP compiler extension') - ]; - $extensions_padding = \ncc\Utilities\Functions::detectParametersPadding($extensions) + 2; - - Console::out('Extensions:'); - foreach($extensions as $command) - { - Console::out(' ' . $command->toString($extensions_padding)); - } + Console::outHelpSections([ + new CliHelpSection(['build'], 'Builds the current project'), + new CliHelpSection(['exec'], 'Executes the main entrypoint of a package') + ]); } } \ No newline at end of file diff --git a/src/ncc/CLI/Main.php b/src/ncc/CLI/Main.php index 793a8fc..5ce5ad0 100644 --- a/src/ncc/CLI/Main.php +++ b/src/ncc/CLI/Main.php @@ -1,4 +1,24 @@ getMessage()); + } + } + } \ No newline at end of file diff --git a/src/ncc/CLI/ConfigMenu.php b/src/ncc/CLI/Management/ConfigMenu.php similarity index 79% rename from src/ncc/CLI/ConfigMenu.php rename to src/ncc/CLI/Management/ConfigMenu.php index ec7bc2f..2afc0aa 100644 --- a/src/ncc/CLI/ConfigMenu.php +++ b/src/ncc/CLI/Management/ConfigMenu.php @@ -1,6 +1,26 @@ getVault()->getEntry($name); + + if($entry === null) + { + Console::out('Entry not found.', true, 1); + return; + } + + if($entry->isEncrypted()) + { + $tries = 0; + + while(true) + { + try + { + $password = Console::passwordInput('Password/Secret: '); + if (!$entry->unlock($password)) + { + $tries++; + if ($tries >= 3) + { + Console::outError('Too many failed attempts.', true, 1); + return; + } + + Console::outError('Invalid password.', true, 1); + } + else + { + Console::out('Authentication successful.'); + return; + } + } + catch (RuntimeException $e) + { + Console::outException('Cannot unlock entry.', $e, 1); + return; + } + } + } + + else + { + Console::out('Authentication always successful, entry is not encrypted.'); + } + } + + /** + * Prints the list of entries in the vault + * + * @return void + */ + public static function listEntries(): void + { + $credential_manager = new CredentialManager(); + $entries = $credential_manager->getVault()->getEntries(); + + if(count($entries) === 0) + { + Console::out('No entries found.'); + return; + } + + Console::out('Entries:'); + foreach($entries as $entry) + { + Console::out(sprintf(' - %s (%s)', $entry->getName(), $entry->isEncrypted() ? ' (encrypted)' : '')); + } + + Console::out('Total: ' . count($entries)); + } + + /** + * @param $args + * @return void + */ + public static function addEntry($args): void + { + $ResolvedScope = Resolver::resolveScope(); + + if($ResolvedScope !== Scopes::System) + Console::outError('Insufficient permissions to add entries'); + + // Really dumb-proofing this + $name = $args['alias'] ?? $args['name'] ?? null; + $auth_type = $args['auth-type'] ?? $args['auth'] ?? null; + $username = $args['username'] ?? $args['usr'] ?? null; + $password = $args['password'] ?? $args['pwd'] ?? null; + $token = $args['token'] ?? $args['pat'] ?? $args['private-token'] ?? null; + $encrypt = !isset($args['no-encryption']); + + if($name === null) + $name = Console::getInput('Enter a name for the entry: '); + + if($auth_type === null) + $auth_type = Console::getInput('Enter the authentication type (login or pat): '); + + if($auth_type === 'login') + { + if($username === null) + $username = Console::getInput('Username: '); + + if($password === null) + $password = Console::passwordInput('Password: '); + } + elseif($auth_type === 'pat') + { + if($token === null) + $token = Console::passwordInput('Token: '); + } + else + { + Console::outError('Invalid authentication type'); + } + + if($name === null) + { + Console::outError('You must specify a name for the entry (alias, name)', true, 1); + return; + } + + if($auth_type === null) + { + Console::outError('You must specify an authentication type for the entry (auth-type, auth)', true, 1); + return; + } + + $encrypt = Functions::cbool($encrypt); + + switch($auth_type) + { + case 'login': + + if($username === null) + { + Console::outError('You must specify a username for the entry (username, usr)', true, 1); + return; + } + if($password === null) + { + Console::outError('You must specify a password for the entry (password, pwd)', true, 1); + return; + } + + $pass_object = new UsernamePassword(); + $pass_object->setUsername($username); + $pass_object->setPassword($password); + + break; + + case 'pat': + + if($token === null) + { + Console::outError('You must specify a token for the entry (token, pat, private-token)', true, 1); + return; + } + + $pass_object = new AccessToken(); + $pass_object->setAccessToken($token); + + break; + + default: + Console::outError('Invalid authentication type specified', true, 1); + return; + } + + $credential_manager = new CredentialManager(); + if(!$credential_manager->getVault()->addEntry($name, $pass_object, $encrypt)) + { + Console::outError('Failed to add entry, entry already exists.', true, 1); + return; + } + + try + { + $credential_manager->saveVault(); + } + catch(Exception $e) + { + Console::outException('Failed to save vault', $e, 1); + return; + } + + Console::out('Successfully added entry', true, 0); + } + + /** + * Removes an existing entry from the vault. + * + * @param $args + * @return void + */ + private static function removeEntry($args): void + { + $ResolvedScope = Resolver::resolveScope(); + + if($ResolvedScope !== Scopes::System) + Console::outError('Insufficient permissions to remove entries'); + + $name = $args['alias'] ?? $args['name'] ?? null; + + if($name === null) + $name = Console::getInput('Enter the name of the entry to remove: '); + + $credential_manager = new CredentialManager(); + if(!$credential_manager->getVault()->deleteEntry($name)) + { + Console::outError('Failed to remove entry, entry does not exist.', true, 1); + return; + } + + try + { + $credential_manager->saveVault(); + } + catch(Exception $e) + { + Console::outException('Failed to save vault', $e, 1); + return; + } + + Console::out('Successfully removed entry', true, 0); + } + + /** + * Displays the main options section + * + * @return void + */ + private static function displayOptions(): void + { + Console::out('Usage: ncc cred {command} [options]'); + Console::out('Options:'); + Console::outHelpSections([ + new CliHelpSection(['help'], 'Displays this help menu about the value command'), + new CliHelpSection(['add'], 'Adds a new entry to the vault (See below)'), + new CliHelpSection(['remove', '--name'], 'Removes an entry from the vault'), + new CliHelpSection(['list'], 'Lists all entries in the vault'), + ]); + Console::out((string)null); + + Console::out('If you are adding a new entry, you can run the add command in interactive mode'); + Console::out('or you can specify the options below' . PHP_EOL); + + Console::out('Add Options:'); + Console::outHelpSections([ + new CliHelpSection(['--name'], 'The name of the entry'), + new CliHelpSection(['--auth-type', '--auth'], 'The type of authentication (login, pat)'), + new CliHelpSection(['--no-encryption'], 'Omit encryption to the entry (By default it\'s encrypted)', true), + ]); + + Console::out(' login authentication type options:'); + Console::outHelpSections([ + new CliHelpSection(['--username', '--usr'], 'The username for the entry'), + new CliHelpSection(['--password', '--pwd'], 'The password for the entry'), + ]); + + Console::out(' pat authentication type options:'); + Console::outHelpSections([ + new CliHelpSection(['--token', '--pat',], 'The private access token for the entry', true), + ]); + + Console::out('Authentication Types:'); + Console::out(' login'); + Console::out(' pat' . PHP_EOL); + + Console::out('Examples:'); + Console::out(' ncc cred add --alias "My Alias" --auth-type login --username "myusername" --password "mypassword"'); + Console::out(' ncc cred add --alias "My Alias" --auth-type pat --token "mytoken" --no-encryption'); + Console::out(' ncc cred remove --alias "My Alias"'); + } + } \ No newline at end of file diff --git a/src/ncc/CLI/PackageManagerMenu.php b/src/ncc/CLI/Management/PackageManagerMenu.php similarity index 65% rename from src/ncc/CLI/PackageManagerMenu.php rename to src/ncc/CLI/Management/PackageManagerMenu.php index a66d117..5dfbc49 100644 --- a/src/ncc/CLI/PackageManagerMenu.php +++ b/src/ncc/CLI/Management/PackageManagerMenu.php @@ -1,15 +1,36 @@ MagicBytes?->toArray() ?? []), JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)); + Console::out('header: ' . json_encode(($package->Header?->toArray() ?? []), JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)); + Console::out('assembly: ' . json_encode(($package->Assembly?->toArray() ?? []), JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)); + Console::out('main: ' . ($package->MainExecutionPolicy ?? 'N/A')); + Console::out('installer: ' . ($package->Installer?->toArray() ?? 'N/A')); + + if($package->Dependencies !== null && count($package->Dependencies) > 0) + { + Console::out('dependencies:'); + foreach($package->Dependencies as $dependency) + { + Console::out(' - ' . json_encode($dependency->toArray(), JSON_UNESCAPED_SLASHES)); + } + } + else + { + Console::out('dependencies: N/A'); + } + + if($package->ExecutionUnits !== null && count($package->ExecutionUnits) > 0) + { + Console::out('execution_units:'); + foreach($package->ExecutionUnits as $unit) + { + Console::out(' - ' . json_encode($unit->toArray(), JSON_UNESCAPED_SLASHES)); + } + } + else + { + Console::out('execution_units: N/A'); + } + + if($package->Resources !== null && count($package->Resources) > 0) + { + Console::out('resources:'); + foreach($package->Resources as $resource) + { + Console::out(' - ' . sprintf('%s - (%s)', $resource->Name, Functions::b2u(strlen($resource->Data)))); + } + } + else + { + Console::out('resources: N/A'); + } + + if($package->Components !== null && count($package->Components) > 0) + { + Console::out('components:'); + foreach($package->Components as $component) + { + Console::out(' - ' . sprintf('#%s %s - %s', $component->DataType, $component->Name, json_encode(($component->Flags ?? []), JSON_UNESCAPED_SLASHES))); + } + } + else + { + Console::out('components: N/A'); + } + + exit(0); + } + /** * Displays all installed packages * @@ -156,6 +276,7 @@ $package_version = $package_manager->getPackageVersion($package, $version); if($package_version == null) continue; + Console::out(sprintf('%s=%s (%s)', Console::formatColor($package, ConsoleColors::LightGreen), Console::formatColor($version, ConsoleColors::LightMagenta), @@ -165,9 +286,10 @@ catch(Exception $e) { unset($e); - Console::out(sprintf('%s=%s', + Console::out(sprintf('%s=%s (%s)', Console::formatColor($package, ConsoleColors::LightGreen), - Console::formatColor($version, ConsoleColors::LightMagenta) + Console::formatColor($version, ConsoleColors::LightMagenta), + Console::formatColor('N/A', ConsoleColors::LightRed) )); } } @@ -191,33 +313,66 @@ return; } - $path = $package; - $parsed_source = new RemotePackageInput($path); - if($parsed_source->Vendor !== null && $parsed_source->Package !== null) + // check if authentication is provided + $entry_arg = ($args['auth'] ?? null); + $credential_manager = new CredentialManager(); + + if($entry_arg !== null) { - if($parsed_source->Source == null) + $credential = $credential_manager->getVault()->getEntry($entry_arg); + + if($credential == null) { - Console::outError('No source specified', true, 1); + Console::outError(sprintf('Unknown credential entry \'%s\'', $entry_arg), true, 1); return; } + } + else + { + $credential = null; + } - switch($parsed_source->Source) + if($credential !== null && !$credential->isCurrentlyDecrypted()) + { + // Try 3 times + for($i = 0; $i < 3; $i++) { - case RemoteSource::Composer: - try - { - $path = ComposerSource::fetch($parsed_source); - break; - } - catch(Exception $e) - { - Console::outException(sprintf('Failed to fetch package %s', $package), $e, 1); - return; - } - - default: - Console::outError('Cannot install package from source: ' . $parsed_source->Source, true, 1); + try + { + $credential->unlock(Console::passwordInput(sprintf('Enter Password for %s: ', $credential->getName()))); + } + catch (RuntimeException $e) + { + Console::outException(sprintf('Failed to unlock credential %s', $credential->getName()), $e, 1); return; + } + + if($credential->isCurrentlyDecrypted()) + break; + + Console::outWarning(sprintf('Invalid password, %d attempts remaining', 2 - $i)); + } + + if(!$credential->isCurrentlyDecrypted()) + { + Console::outError('Failed to unlock credential', true, 1); + return; + } + } + + $path = $package; + $parsed_source = new RemotePackageInput($path); + + if($parsed_source->Vendor !== null && $parsed_source->Package !== null && $parsed_source->Source !== null) + { + try + { + $path = $package_manager->fetchFromSource($parsed_source->toString(), $credential); + } + catch (Exception $e) + { + Console::outException('Failed to fetch package from source', $e, 1); + return; } } @@ -230,6 +385,18 @@ $user_confirmation = (bool)($args['y'] ?? $args['Y']); } + $installer_options = []; + + if((Functions::cbool($args['skip-dependencies'] ?? false))) + { + $installer_options[] = InstallPackageOptions::SkipDependencies; + } + + if(Functions::cbool($args['reinstall'] ?? false)) + { + $installer_options[] = InstallPackageOptions::Reinstall; + } + try { $package = Package::load($path); @@ -261,38 +428,39 @@ Console::out(' Trademark: ' . Console::formatColor($package->Assembly->Trademark, ConsoleColors::LightGreen)); Console::out((string)null); - if(count($package->Dependencies) > 0) + if(count($package->Dependencies) > 0 && !in_array(InstallPackageOptions::Reinstall, $installer_options)) { $dependencies = []; foreach($package->Dependencies as $dependency) { - $require_dependency = true; + $require_dependency = false; - try - { - $dependency_package = $package_manager->getPackage($dependency->Name); - } - catch (PackageLockException $e) - { - unset($e); - $dependency_package = null; - } - - if($dependency_package !== null) + if(!in_array(InstallPackageOptions::SkipDependencies, $installer_options)) { try { - $dependency_version = $dependency_package->getVersion($dependency->Version); + $dependency_package = $package_manager->getPackage($dependency->Name); } - catch (VersionNotFoundException $e) + catch (PackageLockException $e) { unset($e); - $dependency_version = null; + $dependency_package = null; } - if($dependency_version !== null) + if($dependency_package !== null) { - $require_dependency = false; + try + { + $dependency_version = $dependency_package->getVersion($dependency->Version); + } + catch (VersionNotFoundException $e) + { + unset($e); + $dependency_version = null; + } + + if($dependency_version == null) + $require_dependency = true; } } @@ -305,8 +473,11 @@ } } - Console::out('The following dependencies will be installed:'); - Console::out(sprintf('%s', implode(PHP_EOL, $dependencies))); + if($dependencies !== null && count($dependencies) > 0) + { + Console::out('The package requires the following dependencies:'); + Console::out(sprintf('%s', implode(PHP_EOL, $dependencies))); + } } Console::out(sprintf('Extension: %s', @@ -326,11 +497,13 @@ 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); + $package_manager->install($path, $credential, $installer_options); Console::out(sprintf('Package %s installed successfully', $package->Assembly->Package)); } catch(Exception $e) @@ -382,7 +555,6 @@ $version_entry = null; if($version_entry !== null && $package_entry !== null) - /** @noinspection PhpUnhandledExceptionInspection */ /** @noinspection PhpRedundantOptionalArgumentInspection */ $version_entry = $package_entry->getVersion($version_entry, false); @@ -490,9 +662,12 @@ new CliHelpSection(['list'], 'Lists all installed packages on the system'), new CliHelpSection(['install', '--package', '-p'], 'Installs a specified NCC package'), new CliHelpSection(['install', '--package', '-p', '--version', '-v'], 'Installs a specified NCC package version'), + new CliHelpSection(['install', '-p', '--skip-dependencies'], 'Installs a specified NCC package but skips the installation of dependencies'), + new CliHelpSection(['install', '-p', '--reinstall'], 'Installs a specified NCC package, reinstall if already installed'), new CliHelpSection(['uninstall', '--package', '-p'], 'Uninstalls a specified NCC package'), new CliHelpSection(['uninstall', '--package', '-p', '--version', '-v'], 'Uninstalls a specified NCC package version'), new CliHelpSection(['uninstall-all'], 'Uninstalls all packages'), + new CliHelpSection(['sdc', '--package', '-p'], '(Debug) Semi-decompiles a specified NCC package and prints the result to the console'), ]; $options_padding = Functions::detectParametersPadding($options) + 4; diff --git a/src/ncc/CLI/ProjectMenu.php b/src/ncc/CLI/Management/ProjectMenu.php similarity index 80% rename from src/ncc/CLI/ProjectMenu.php rename to src/ncc/CLI/Management/ProjectMenu.php index 9f8eaef..b7ce09e 100644 --- a/src/ncc/CLI/ProjectMenu.php +++ b/src/ncc/CLI/Management/ProjectMenu.php @@ -1,18 +1,45 @@ getSources(); + + if(count($sources) == 0) + { + Console::out('No remote sources defined.', 1); + return; + } + + Console::out('Remote sources:', 1); + foreach($sources as $source) + { + Console::out(' - ' . $source->Name . ' (' . $source->Host . ')', 1); + } + + Console::out('Total: ' . count($sources), 1); + } + + /** + * @param $args + * @return void + */ + public static function addEntry($args): void + { + if(Resolver::resolveScope() !== Scopes::System) + { + Console::outError('Insufficient permissions to add entry.', true, 1); + return; + } + + $name = $args['name'] ?? null; + $type = $args['type'] ?? null; + $host = $args['host'] ?? null; + $ssl = $args['ssl'] ?? null; + + if($name == null) + { + Console::outError(sprintf('Missing required argument \'%s\'.', 'name'), true, 1); + return; + } + + if($type == null) + { + Console::outError(sprintf('Missing required argument \'%s\'.', 'type'), true, 1); + return; + } + + if($host == null) + { + Console::outError(sprintf('Missing required argument \'%s\'.', 'host'), true, 1); + return; + } + + if($ssl !== null) + { + $ssl = Functions::cbool($ssl); + } + + $source_manager = new RemoteSourcesManager(); + $source = new DefinedRemoteSource(); + $source->Name = $name; + $source->Type = $type; + $source->Host = $host; + $source->SSL = $ssl; + + if(!$source_manager->addRemoteSource($source)) + { + Console::outError(sprintf('Cannot add entry \'%s\', it already exists', $name), true, 1); + return; + } + + try + { + $source_manager->save(); + } + catch (IOException $e) + { + Console::outException('Cannot save remote sources file.', $e, 1); + return; + } + + Console::out(sprintf('Entry \'%s\' added successfully.', $name)); + } + + /** + * Removes an existing entry from the vault. + * + * @param $args + * @return void + */ + private static function removeEntry($args): void + { + $ResolvedScope = Resolver::resolveScope(); + + if($ResolvedScope !== Scopes::System) + Console::outError('Insufficient permissions to remove entries'); + + $name = $args['name'] ?? null; + + if($name == null) + { + Console::outError(sprintf('Missing required argument \'%s\'.', 'name'), true, 1); + return; + } + + $source_manager = new RemoteSourcesManager(); + + if(!$source_manager->deleteRemoteSource($name)) + { + Console::outError(sprintf('Cannot remove entry \'%s\', it does not exist', $name), true, 1); + return; + } + + try + { + $source_manager->save(); + } + catch (IOException $e) + { + Console::outException('Cannot save remote sources file.', $e, 1); + return; + + } + Console::out(sprintf('Entry \'%s\' removed successfully.', $name)); + } + + /** + * Displays the main options section + * + * @return void + */ + private static function displayOptions(): void + { + Console::out('Usage: ncc sources {command} [options]'); + Console::out('Options:'); + Console::outHelpSections([ + new CliHelpSection(['help'], 'Displays this help menu about the sources command'), + new CliHelpSection(['add'], 'Adds a new entry to the list of remote sources (See below)'), + new CliHelpSection(['remove', '--name'], 'Removes an entry from the list'), + new CliHelpSection(['list'], 'Lists all entries defined as remote sources'), + ]); + Console::out((string)null); + + } + } \ No newline at end of file diff --git a/src/ncc/CLI/PhpMenu.php b/src/ncc/CLI/PhpMenu.php deleted file mode 100644 index 1c10f39..0000000 --- a/src/ncc/CLI/PhpMenu.php +++ /dev/null @@ -1,59 +0,0 @@ -toString($options_padding)); - } - } - } \ No newline at end of file diff --git a/src/ncc/Classes/BashExtension/BashRunner.php b/src/ncc/Classes/BashExtension/BashRunner.php new file mode 100644 index 0000000..c10b993 --- /dev/null +++ b/src/ncc/Classes/BashExtension/BashRunner.php @@ -0,0 +1,56 @@ +Execute->Target = null; + $execution_unit->ExecutionPolicy = $policy; + $execution_unit->Data = IO::fread($path); + + return $execution_unit; + } + + /** + * @inheritDoc + */ + public static function getFileExtension(): string + { + return '.bash'; + } + } \ No newline at end of file diff --git a/src/ncc/Classes/ComposerExtension/ComposerSource.php b/src/ncc/Classes/ComposerExtension/ComposerSourceBuiltin.php similarity index 54% rename from src/ncc/Classes/ComposerExtension/ComposerSource.php rename to src/ncc/Classes/ComposerExtension/ComposerSourceBuiltin.php index d732939..a70bb18 100644 --- a/src/ncc/Classes/ComposerExtension/ComposerSource.php +++ b/src/ncc/Classes/ComposerExtension/ComposerSourceBuiltin.php @@ -1,6 +1,26 @@ toStandard())); } + /** + * Works with a local composer.json file and attempts to compile the required packages + * and their dependencies, returns the path to the compiled package. + * + * @param string $path + * @return string + * @throws AccessDeniedException + * @throws BuildConfigurationNotFoundException + * @throws BuildException + * @throws ComposerDisabledException + * @throws ComposerException + * @throws ComposerNotAvailableException + * @throws DirectoryNotFoundException + * @throws FileNotFoundException + * @throws IOException + * @throws InternalComposerNotAvailableException + * @throws MalformedJsonException + * @throws PackageNotFoundException + * @throws PackagePreparationFailedException + * @throws ProjectConfigurationNotFoundException + * @throws UnsupportedCompilerExtensionException + * @throws UserAbortedOperationException + */ + public static function fromLocal(string $path): string + { + // Check if the file composer.json exists + if (!file_exists($path . DIRECTORY_SEPARATOR . 'composer.json')) + throw new FileNotFoundException(sprintf('File "%s" not found', $path . DIRECTORY_SEPARATOR . 'composer.json')); + + // Execute composer with options + $options = self::getOptions(); + $composer_exec = self::getComposerPath(); + $process = new Process([$composer_exec, 'install']); + self::prepareProcess($process, $path, $options); + + Console::outDebug(sprintf('executing %s', $process->getCommandLine())); + $process->run(function ($type, $buffer) { + Console::out($buffer, false); + }); + + if (!$process->isSuccessful()) + throw new ComposerException($process->getErrorOutput()); + + $filesystem = new Filesystem(); + if($filesystem->exists($path . DIRECTORY_SEPARATOR . 'build')) + $filesystem->remove($path . DIRECTORY_SEPARATOR . 'build'); + $filesystem->mkdir($path . DIRECTORY_SEPARATOR . 'build'); + + // Compile dependencies + self::compilePackages($path . DIRECTORY_SEPARATOR . 'composer.lock'); + + $composer_lock = Functions::loadJson(IO::fread($path . DIRECTORY_SEPARATOR . 'composer.lock'), Functions::FORCE_ARRAY); + $version_map = self::getVersionMap(ComposerLock::fromArray($composer_lock)); + + // Finally convert the main package's composer.json to package.json and compile it + ComposerSourceBuiltin::convertProject($path, $version_map); + $project_manager = new ProjectManager($path); + $project_manager->load(); + $built_package = $project_manager->build(); + + RuntimeCache::setFileAsTemporary($built_package); + return $built_package; + } + /** * @param string $composer_lock_path * @return array @@ -109,7 +193,6 @@ * @throws PackagePreparationFailedException * @throws ProjectConfigurationNotFoundException * @throws UnsupportedCompilerExtensionException - * @throws UnsupportedRunnerException */ private static function compilePackages(string $composer_lock_path): array { @@ -129,108 +212,19 @@ } $filesystem->mkdir($base_dir . DIRECTORY_SEPARATOR . 'build'); + $version_map = self::getVersionMap($composer_lock); foreach ($composer_lock->Packages as $package) { $package_path = $base_dir . DIRECTORY_SEPARATOR . 'vendor' . DIRECTORY_SEPARATOR . $package->Name; - // Generate the package configuration - $project_configuration = ComposerSource::generateProjectConfiguration($package->Name, $composer_lock); - // Process the source files - if ($package->Autoload !== null) - { - $source_directory = $package_path . DIRECTORY_SEPARATOR . '.src'; - if ($filesystem->exists($source_directory)) - { - $filesystem->remove($source_directory); - } - $filesystem->mkdir($source_directory); - $source_directories = []; - $static_files = []; + // Load the composer lock file + $composer_package = $composer_lock->getPackage($package->Name); + if ($composer_package == null) + throw new PackageNotFoundException(sprintf('Package "%s" not found in composer lock file', $package->Name)); - // TODO: Implement static files handling - - // Extract all the source directories - if ($package->Autoload->Psr4 !== null && count($package->Autoload->Psr4) > 0) - { - Console::outVerbose('Extracting PSR-4 source directories'); - foreach ($package->Autoload->Psr4 as $namespace_pointer) - { - if ($namespace_pointer->Path !== null && !in_array($namespace_pointer->Path, $source_directories)) - { - $source_directories[] = $package_path . DIRECTORY_SEPARATOR . $namespace_pointer->Path; - } - } - } - - if ($package->Autoload->Psr0 !== null && count($package->Autoload->Psr0) > 0) - { - Console::outVerbose('Extracting PSR-0 source directories'); - foreach ($package->Autoload->Psr0 as $namespace_pointer) - { - if ($namespace_pointer->Path !== null && !in_array($namespace_pointer->Path, $source_directories)) - { - $source_directories[] = $package_path . DIRECTORY_SEPARATOR . $namespace_pointer->Path; - } - } - } - - if ($package->Autoload->Files !== null && count($package->Autoload->Files) > 0) - { - Console::outVerbose('Extracting static files'); - foreach ($package->Autoload->Files as $file) - { - $static_files[] = $package_path . DIRECTORY_SEPARATOR . $file; - } - } - - Console::outDebug(sprintf('source directories: %s', implode(', ', $source_directories))); - - // First scan the project files and create a file struct. - $DirectoryScanner = new DirectoryScanner(); - - // TODO: Implement exclude-class handling - try - { - $DirectoryScanner->unsetFlag(FilesystemIterator::FOLLOW_SYMLINKS); - } - catch (Exception $e) - { - throw new PackagePreparationFailedException('Cannot unset flag \'FOLLOW_SYMLINKS\' in DirectoryScanner, ' . $e->getMessage(), $e); - } - - // Include file components that can be compiled - $DirectoryScanner->setIncludes(ComponentFileExtensions::Php); - - foreach ($source_directories as $directory) - { - /** @var SplFileInfo $item */ - /** @noinspection PhpRedundantOptionalArgumentInspection */ - foreach ($DirectoryScanner($directory, True) as $item) - { - if (is_dir($item->getPathName())) - continue; - - $parsed_path = str_ireplace($package_path . DIRECTORY_SEPARATOR, '', $item->getPathName()); - - Console::outDebug(sprintf('copying file %s for package %s', $parsed_path, $package->Name)); - $filesystem->copy($item->getPathName(), $source_directory . DIRECTORY_SEPARATOR . $parsed_path); - } - } - - if (count($static_files) > 0) - { - $project_configuration->Project->Options['static_files'] = $static_files; - $parsed_path = str_ireplace($package_path . DIRECTORY_SEPARATOR, '', $item->getPathName()); - if (!$filesystem->exists($source_directory . DIRECTORY_SEPARATOR . $parsed_path)) - { - Console::outDebug(sprintf('copying file %s for package %s', $parsed_path, $package->Name)); - $filesystem->copy($item->getPathName(), $source_directory . DIRECTORY_SEPARATOR . $parsed_path); - } - } - - $project_configuration->toFile($package_path . DIRECTORY_SEPARATOR . 'project.json'); - } + // Convert it to a NCC project configuration + $project_configuration = self::convertProject($package_path, $version_map, $composer_package); // Load the project $project_manager = new ProjectManager($package_path); @@ -238,15 +232,31 @@ $built_package = $project_manager->build(); // Copy the project to the build directory - $out_path = $base_dir . DIRECTORY_SEPARATOR . 'build' . DIRECTORY_SEPARATOR . sprintf('%s=%s.ncc', $project_configuration->Assembly->Package, $project_configuration->Assembly->Version); + $out_path = $base_dir . DIRECTORY_SEPARATOR . 'build' . DIRECTORY_SEPARATOR . sprintf('%s.ncc', $project_configuration->Assembly->Package); $filesystem->copy($built_package, $out_path); + $filesystem->remove($built_package); $built_packages[$project_configuration->Assembly->Package] = $out_path; - } return $built_packages; } + /** + * Returns array of versions from the ComposerLock file + * + * @param ComposerLock $composerLock + * @return array + */ + private static function getVersionMap(ComposerLock $composerLock): array + { + $version_map = []; + foreach($composerLock->Packages as $package) + { + $version_map[$package->Name] = $package->Version; + } + return $version_map; + } + /** * Converts a composer package name to a valid package name * @@ -271,22 +281,33 @@ return null; } + /** + * Returns a valid version from a version map + * + * @param string $package_name + * @param array $version_map + * @return string + */ + private static function versionMap(string $package_name, array $version_map): string + { + if (array_key_exists($package_name, $version_map)) + { + return Functions::convertToSemVer($version_map[$package_name]); + } + + return '1.0.0'; + } + /** * Generates a project configuration from a package selection * from the composer.lock file * - * @param string $package_name - * @param ComposerLock $composer_lock + * @param ComposerJson $composer_package + * @param array $version_map * @return ProjectConfiguration - * @throws PackageNotFoundException */ - private static function generateProjectConfiguration(string $package_name, ComposerLock $composer_lock): ProjectConfiguration + private static function generateProjectConfiguration(ComposerJson $composer_package, array $version_map): ProjectConfiguration { - // Load the composer lock file - $composer_package = $composer_lock->getPackage($package_name); - if ($composer_package == null) - throw new PackageNotFoundException(sprintf('Package "%s" not found in composer lock file', $package_name)); - // Generate a new project configuration object $project_configuration = new ProjectConfiguration(); @@ -294,37 +315,41 @@ $project_configuration->Assembly->Name = $composer_package->Name; if (isset($composer_package->Description)) $project_configuration->Assembly->Description = $composer_package->Description; - if (isset($composer_package->Version)) - $project_configuration->Assembly->Version = Functions::parseVersion($composer_package->Version); + + if(isset($version_map[$composer_package->Name])) + $project_configuration->Assembly->Version = self::versionMap($composer_package->Name, $version_map); + if($project_configuration->Assembly->Version == null || $project_configuration->Assembly->Version == '') + $project_configuration->Assembly->Version = '1.0.0'; + $project_configuration->Assembly->UUID = Uuid::v1()->toRfc4122(); - $project_configuration->Assembly->Package = self::toPackageName($package_name); + $project_configuration->Assembly->Package = self::toPackageName($composer_package->Name); + + // Add the update source + $project_configuration->Project->UpdateSource = new ProjectConfiguration\UpdateSource(); + $project_configuration->Project->UpdateSource->Source = sprintf('%s@composer', str_ireplace('\\', '/', $composer_package->Name)); + $project_configuration->Project->UpdateSource->Repository = null; // Process the dependencies - foreach ($composer_package->Require as $item) + if($composer_package->Require !== null && count($composer_package->Require) > 0) { - $package_name = self::toPackageName($item->PackageName); - $package_version = $composer_lock->getPackage($item->PackageName)?->Version; - if ($package_version == null) + foreach ($composer_package->Require as $item) { - $package_version = '1.0.0'; + // Check if the dependency is already in the project configuration + $package_name = self::toPackageName($item->PackageName); + if ($package_name == null) + continue; + $dependency = new ProjectConfiguration\Dependency(); + $dependency->Name = $package_name; + $dependency->SourceType = DependencySourceType::Local; + $dependency->Version = self::versionMap($item->PackageName, $version_map); + $dependency->Source = $package_name . '.ncc'; + $project_configuration->Build->addDependency($dependency); } - else - { - $package_version = Functions::parseVersion($package_version); - } - if ($package_name == null) - continue; - $dependency = new ProjectConfiguration\Dependency(); - $dependency->Name = $package_name; - $dependency->SourceType = DependencySourceType::Local; - $dependency->Version = $package_version; - $dependency->Source = $package_name . '=' . $dependency->Version . '.ncc'; - $project_configuration->Build->Dependencies[] = $dependency; } // Create a build configuration - $build_configuration = new ProjectConfiguration\BuildConfiguration(); + $build_configuration = new ProjectConfiguration\Build\BuildConfiguration(); $build_configuration->Name = 'default'; $build_configuration->OutputPath = 'build'; @@ -341,22 +366,6 @@ return $project_configuration; } - /** - * Extracts a version if available from the input - * - * @param string $input - * @return string|null - */ - private static function extractVersion(string $input): ?string - { - if (stripos($input, ':')) - { - return explode(':', $input)[1]; - } - - return null; - } - /** * Gets the applicable options configured for composer * @@ -437,7 +446,6 @@ * @param string $vendor * @param string $package * @param string|null $version - * @param array $options * @return string * @throws AccessDeniedException * @throws ComposerDisabledException @@ -449,13 +457,15 @@ * @throws InvalidScopeException * @throws UserAbortedOperationException */ - private static function require(string $vendor, string $package, ?string $version = null, array $options = []): string + private static function require(string $vendor, string $package, ?string $version = null): string { if (Resolver::resolveScope() !== Scopes::System) throw new AccessDeniedException('Insufficient permissions to require'); if ($version == null) $version = '*'; + if($version == 'latest') + $version = '*'; $tpl_file = __DIR__ . DIRECTORY_SEPARATOR . 'composer.jtpl'; if (!file_exists($tpl_file)) @@ -482,29 +492,11 @@ // Execute composer with options $options = self::getOptions(); $process = new Process(array_merge([$composer_exec, 'require'], $options)); - $process->setWorkingDirectory($tmp_dir); - - // Check if scripts are enabled while running as root - if (!in_array('--no-scripts', $options) && Resolver::resolveScope() == Scopes::System) - { - Console::outWarning('composer scripts are enabled while running as root, this can allow malicious scripts to run as root'); - if (!isset($options['--no-interaction'])) - { - if (!Console::getBooleanInput('Do you want to continue?')) - throw new UserAbortedOperationException('The operation was aborted by the user'); - - // The user understands the risks and wants to continue - $process->setEnv(['COMPOSER_ALLOW_SUPERUSER' => 1]); - } - } - else - { - // Composer is running "safely". We can disable the superuser check - $process->setEnv(['COMPOSER_ALLOW_SUPERUSER' => 1]); - } + self::prepareProcess($process, $tmp_dir, $options); Console::outDebug(sprintf('executing %s', $process->getCommandLine())); - $process->run(function ($type, $buffer) { + $process->run(function ($type, $buffer) + { Console::out($buffer, false); }); @@ -558,4 +550,206 @@ throw new ComposerNotAvailableException('No composer executable path is configured'); } + + /** + * @param Process $process + * @param string $path + * @param array $options + * @return void + * @throws UserAbortedOperationException + */ + private static function prepareProcess(Process $process, string $path, array $options): void + { + $process->setWorkingDirectory($path); + + // Check if scripts are enabled while running as root + if (!in_array('--no-scripts', $options) && Resolver::resolveScope() == Scopes::System) + { + Console::outWarning('composer scripts are enabled while running as root, this can allow malicious scripts to run as root'); + if (!isset($options['--no-interaction'])) + { + if (!Console::getBooleanInput('Do you want to continue?')) + throw new UserAbortedOperationException('The operation was aborted by the user'); + + // The user understands the risks and wants to continue + $process->setEnv(['COMPOSER_ALLOW_SUPERUSER' => 1]); + } + } + else + { + // Composer is running "safely". We can disable the superuser check + $process->setEnv(['COMPOSER_ALLOW_SUPERUSER' => 1]); + } + } + + /** + * Converts a composer project to a NCC project + * + * @param string $package_path + * @param array $version_map + * @param mixed $composer_package + * @return ProjectConfiguration + * @throws AccessDeniedException + * @throws FileNotFoundException + * @throws IOException + * @throws MalformedJsonException + * @throws PackagePreparationFailedException + */ + private static function convertProject(string $package_path, array $version_map, ?ComposerJson $composer_package=null): ProjectConfiguration + { + if($composer_package == null) + $composer_package = ComposerJson::fromArray(Functions::loadJsonFile($package_path . DIRECTORY_SEPARATOR . 'composer.json', Functions::FORCE_ARRAY)); + + $project_configuration = ComposerSourceBuiltin::generateProjectConfiguration($composer_package, $version_map); + $filesystem = new Filesystem(); + + // Process the source files + if ($composer_package->Autoload !== null) + { + $source_directory = $package_path . DIRECTORY_SEPARATOR . '.src'; + if ($filesystem->exists($source_directory)) + $filesystem->remove($source_directory); + $filesystem->mkdir($source_directory); + $source_directories = []; + $static_files = []; + + // Extract all the source directories + if ($composer_package->Autoload->Psr4 !== null && count($composer_package->Autoload->Psr4) > 0) + { + Console::outVerbose('Extracting PSR-4 source directories'); + foreach ($composer_package->Autoload->Psr4 as $namespace_pointer) + { + if ($namespace_pointer->Path !== null && !in_array($namespace_pointer->Path, $source_directories)) + { + $source_directories[] = $package_path . DIRECTORY_SEPARATOR . $namespace_pointer->Path; + } + } + } + + if ($composer_package->Autoload->Psr0 !== null && count($composer_package->Autoload->Psr0) > 0) + { + Console::outVerbose('Extracting PSR-0 source directories'); + foreach ($composer_package->Autoload->Psr0 as $namespace_pointer) + { + if ($namespace_pointer->Path !== null && !in_array($namespace_pointer->Path, $source_directories)) + { + $source_directories[] = $package_path . DIRECTORY_SEPARATOR . $namespace_pointer->Path; + } + } + } + + if ($composer_package->Autoload->Files !== null && count($composer_package->Autoload->Files) > 0) + { + Console::outVerbose('Extracting static files'); + foreach ($composer_package->Autoload->Files as $file) + { + $static_files[] = $package_path . DIRECTORY_SEPARATOR . $file; + } + } + + Console::outDebug(sprintf('source directories: %s', implode(', ', $source_directories))); + + // First scan the project files and create a file struct. + $DirectoryScanner = new DirectoryScanner(); + + // TODO: Implement exclude-class handling + try + { + $DirectoryScanner->unsetFlag(FilesystemIterator::FOLLOW_SYMLINKS); + } + catch (Exception $e) + { + throw new PackagePreparationFailedException('Cannot unset flag \'FOLLOW_SYMLINKS\' in DirectoryScanner, ' . $e->getMessage(), $e); + } + + // Include file components that can be compiled + $DirectoryScanner->setIncludes(ComponentFileExtensions::Php); + + foreach ($source_directories as $directory) + { + /** @var SplFileInfo $item */ + /** @noinspection PhpRedundantOptionalArgumentInspection */ + foreach ($DirectoryScanner($directory, True) as $item) + { + if (is_dir($item->getPathName())) + continue; + + $parsed_path = str_ireplace($package_path . DIRECTORY_SEPARATOR, '', $item->getPathName()); + + Console::outDebug(sprintf('copying file %s for package %s', $parsed_path, $composer_package->Name)); + $filesystem->copy($item->getPathName(), $source_directory . DIRECTORY_SEPARATOR . $parsed_path); + } + } + + if (count($static_files) > 0) + { + $project_configuration->Project->Options['static_files'] = $static_files; + + foreach ($static_files as $file) + { + $parsed_path = str_ireplace($package_path . DIRECTORY_SEPARATOR, '', $file); + Console::outDebug(sprintf('copying file %s for package %s', $parsed_path, $composer_package->Name)); + $filesystem->copy($file, $source_directory . DIRECTORY_SEPARATOR . $parsed_path); + } + unset($file); + } + } + + $project_configuration->toFile($package_path . DIRECTORY_SEPARATOR . 'project.json'); + + // This part simply displays the package information to the command-line interface + if(ncc::cliMode()) + { + $license_files = [ + 'LICENSE', + 'license', + 'LICENSE.txt', + 'license.txt' + ]; + + foreach($license_files as $license_file) + { + if($filesystem->exists($package_path . DIRECTORY_SEPARATOR . $license_file)) + { + // Check configuration if composer.extension.display_licenses is set + if(Functions::cbool(Functions::getConfigurationProperty('composer.extension.display_licenses'))) + { + Console::out(sprintf('License for package %s:', $composer_package->Name)); + Console::out(IO::fread($package_path . DIRECTORY_SEPARATOR . $license_file)); + break; + } + } + } + + if(Functions::cbool(Functions::getConfigurationProperty('composer.extension.display_authors'))) + { + if($composer_package->Authors !== null && count($composer_package->Authors) > 0) + { + Console::out(sprintf('Authors for package %s:', $composer_package->Name)); + foreach($composer_package->Authors as $author) + { + Console::out(sprintf(' - %s', $author->Name)); + + if($author->Email !== null) + { + Console::out(sprintf(' %s', $author->Email)); + } + + if($author->Homepage !== null) + { + Console::out(sprintf(' %s', $author->Homepage)); + } + + if($author->Role !== null) + { + Console::out(sprintf(' %s', $author->Role)); + } + + } + } + } + } + + return $project_configuration; + } } \ No newline at end of file diff --git a/src/ncc/Classes/EnvironmentConfiguration.php b/src/ncc/Classes/EnvironmentConfiguration.php deleted file mode 100644 index 5910bd0..0000000 --- a/src/ncc/Classes/EnvironmentConfiguration.php +++ /dev/null @@ -1,71 +0,0 @@ - $config) - { - $results[$name] = PhpConfiguration::fromArray($config); - } - - return $results; - } - - /** - * Returns an array of only the changed configuration values - * - * @return PhpConfiguration[] - */ - public static function getChangedValues(): array - { - $results = []; - - foreach(ini_get_all() as $name => $config) - { - $config = PhpConfiguration::fromArray($config); - if($config->LocalValue !== $config->GlobalValue) - { - $results[$name] = $config; - } - } - - return $results; - } - - /** - * @param string $file_path - * @return void - */ - public static function export(string $file_path) - { - $configuration = []; - foreach(self::getChangedValues() as $changedValue) - { - $configuration[$changedValue->getName()] = $changedValue->getValue(); - } - - // TODO: Implement ini writing process here - } - - public static function import(string $file_path) - { - // TODO: Implement ini reading process here - $configuration = []; - foreach($configuration as $item => $value) - { - ini_set($item, $value); - } - } - } \ No newline at end of file diff --git a/src/ncc/Classes/GitClient.php b/src/ncc/Classes/GitClient.php new file mode 100644 index 0000000..6245ed7 --- /dev/null +++ b/src/ncc/Classes/GitClient.php @@ -0,0 +1,132 @@ +setTimeout(3600); // 1 hour + $process->run(function ($type, $buffer) + { + Console::outVerbose($buffer); + }); + + if (!$process->isSuccessful()) + throw new GitCloneException($process->getErrorOutput()); + + Console::outVerbose('Repository cloned to: ' . $path); + + return $path; + } + + /** + * Checks out a specific branch or tag. + * + * @param string $path + * @param string $branch + * @throws GitCheckoutException + */ + public static function checkout(string $path, string $branch): void + { + Console::outVerbose('Checking out branch' . $branch); + $process = new Process(["git", "checkout", $branch], $path); + $process->setTimeout(3600); // 1 hour + $process->run(function ($type, $buffer) + { + if (Process::ERR === $type) + { + Console::outWarning($buffer); + } + else + { + Console::outVerbose($buffer); + } + }); + + if (!$process->isSuccessful()) + throw new GitCheckoutException($process->getErrorOutput()); + + Console::outVerbose('Checked out branch: ' . $branch); + } + + /** + * Returns an array of tags that are available in the repository. + * + * @param string $path + * @return array + * @throws GitTagsException + */ + public static function getTags(string $path): array + { + Console::outVerbose('Getting tags for repository: ' . $path); + $process = new Process(["git", "fetch", '--all', '--tags'] , $path); + $process->setTimeout(3600); // 1 hour + $process->run(function ($type, $buffer) + { + Console::outVerbose($buffer); + }); + + if (!$process->isSuccessful()) + throw new GitTagsException($process->getErrorOutput()); + + $process = new Process(['git', '--no-pager', 'tag', '-l'] , $path); + + $process->run(function ($type, $buffer) + { + Console::outVerbose($buffer); + }); + + if (!$process->isSuccessful()) + throw new GitTagsException($process->getErrorOutput()); + + $tags = explode(PHP_EOL, $process->getOutput()); + $tags = array_filter($tags, function ($tag) + { + return !empty($tag); + }); + + Console::outDebug('found ' . count($tags) . ' tags'); + return array_filter($tags); + } + + } \ No newline at end of file diff --git a/src/ncc/Classes/GithubExtension/GithubService.php b/src/ncc/Classes/GithubExtension/GithubService.php new file mode 100644 index 0000000..4b8bcc2 --- /dev/null +++ b/src/ncc/Classes/GithubExtension/GithubService.php @@ -0,0 +1,277 @@ +SSL ? "https" : "http"); + $owner_f = str_ireplace("/", "%2F", $packageInput->Vendor); + $owner_f = str_ireplace(".", "%2F", $owner_f); + $repository = urlencode($packageInput->Package); + $httpRequest->Url = $protocol . '://' . $definedRemoteSource->Host . "/repos/$owner_f/$repository"; + $response_decoded = self::getJsonResponse($httpRequest, $entry); + + $query = new RepositoryQueryResults(); + $query->Files->GitSshUrl = ($response_decoded['ssh_url'] ?? null); + $query->Files->GitHttpUrl = ($response_decoded['clone_url'] ?? null); + $query->Version = Functions::convertToSemVer($response_decoded['default_branch'] ?? null); + $query->ReleaseDescription = ($response_decoded['description'] ?? null); + $query->ReleaseName = ($response_decoded['name'] ?? null); + + + return $query; + } + + /** + * Returns the download URL of the requested version of the package. + * + * @param RemotePackageInput $packageInput + * @param DefinedRemoteSource $definedRemoteSource + * @param Entry|null $entry + * @return RepositoryQueryResults + * @throws AuthenticationException + * @throws GithubServiceException + * @throws GitlabServiceException + * @throws HttpException + * @throws MalformedJsonException + * @throws VersionNotFoundException + */ + public static function getRelease(RemotePackageInput $packageInput, DefinedRemoteSource $definedRemoteSource, ?Entry $entry = null): RepositoryQueryResults + { + return self::processReleases($packageInput, $definedRemoteSource, $entry); + } + + /** + * @param RemotePackageInput $packageInput + * @param DefinedRemoteSource $definedRemoteSource + * @param Entry|null $entry + * @return RepositoryQueryResults + * @throws AuthenticationException + * @throws GithubServiceException + * @throws GitlabServiceException + * @throws HttpException + * @throws MalformedJsonException + * @throws VersionNotFoundException + */ + public static function getNccPackage(RemotePackageInput $packageInput, DefinedRemoteSource $definedRemoteSource, ?Entry $entry = null): RepositoryQueryResults + { + return self::processReleases($packageInput, $definedRemoteSource, $entry); + } + + /** + * Returns a list of all releases of the given repository with their download URL. + * + * @param RemotePackageInput $packageInput + * @param DefinedRemoteSource $definedRemoteSource + * @param Entry|null $entry + * @return array + * @throws AuthenticationException + * @throws GithubServiceException + * @throws GitlabServiceException + * @throws HttpException + * @throws MalformedJsonException + */ + private static function getReleases(RemotePackageInput $packageInput, DefinedRemoteSource $definedRemoteSource, ?Entry $entry = null): array + { + $httpRequest = new HttpRequest(); + $protocol = ($definedRemoteSource->SSL ? "https" : "http"); + $owner_f = str_ireplace("/", "%2F", $packageInput->Vendor); + $owner_f = str_ireplace(".", "%2F", $owner_f); + $repository = urlencode($packageInput->Package); + $httpRequest->Url = $protocol . '://' . $definedRemoteSource->Host . "/repos/$owner_f/$repository/releases"; + $response_decoded = self::getJsonResponse($httpRequest, $entry); + + if(count($response_decoded) == 0) + return []; + + $return = []; + foreach($response_decoded as $release) + { + $query_results = new RepositoryQueryResults(); + $query_results->Version = Functions::convertToSemVer($release['tag_name']); + $query_results->ReleaseName = $release['name']; + $query_results->ReleaseDescription = $release['body']; + $query_results->Files->ZipballUrl = ($release['zipball_url'] ?? null); + $query_results->Files->TarballUrl = ($release['tarball_url'] ?? null); + + if(isset($release['assets'])) + { + foreach($release['assets'] as $asset) + { + $parsed_asset = self::parseAsset($asset); + if($parsed_asset !== null) + $query_results->Files->PackageUrl = $parsed_asset; + } + } + + $return[$query_results->Version] = $query_results; + } + + return $return; + } + + /** + * Returns the asset download URL if it points to a .ncc package. + * + * @param array $asset + * @return string|null' + */ + private static function parseAsset(array $asset): ?string + { + if(isset($asset['browser_download_url'])) + { + $file_extension = pathinfo($asset['browser_download_url'], PATHINFO_EXTENSION); + if($file_extension == 'ncc') + return $asset['browser_download_url']; + } + + return null; + } + + /** + * @param HttpRequest $httpRequest + * @param Entry|null $entry + * @return array + * @throws AuthenticationException + * @throws GithubServiceException + * @throws GitlabServiceException + * @throws HttpException + * @throws MalformedJsonException + */ + private static function getJsonResponse(HttpRequest $httpRequest, ?Entry $entry): array + { + $httpRequest->Type = HttpRequestType::GET; + $httpRequest = Functions::prepareGitServiceRequest($httpRequest, $entry, false); + $httpRequest->Headers[] = 'X-GitHub-Api-Version: 2022-11-28'; + $httpRequest->Headers[] = 'Accept: application/vnd.github+json'; + + $response = HttpClient::request($httpRequest, true); + + if ($response->StatusCode != 200) + throw new GithubServiceException(sprintf('Failed to fetch releases for the given repository. Status code: %s', $response->StatusCode)); + + return Functions::loadJson($response->Body, Functions::FORCE_ARRAY); + } + + /** + * @param RemotePackageInput $packageInput + * @param DefinedRemoteSource $definedRemoteSource + * @param Entry|null $entry + * @return mixed + * @throws AuthenticationException + * @throws GithubServiceException + * @throws GitlabServiceException + * @throws HttpException + * @throws MalformedJsonException + * @throws VersionNotFoundException + */ + private static function processReleases(RemotePackageInput $packageInput, DefinedRemoteSource $definedRemoteSource, ?Entry $entry): mixed + { + $releases = self::getReleases($packageInput, $definedRemoteSource, $entry); + + if (count($releases) === 0) + throw new VersionNotFoundException('No releases found for the given repository.'); + + if ($packageInput->Version == Versions::Latest) + { + $latest_version = null; + foreach ($releases as $release) + { + if ($latest_version == null) + { + $latest_version = $release->Version; + continue; + } + + if (VersionComparator::compareVersion($release->Version, $latest_version) == 1) + $latest_version = $release->Version; + } + + return $releases[$latest_version]; + } + + // Query a specific version + if (!isset($releases[$packageInput->Version])) + { + // Find the closest thing to the requested version + $selected_version = null; + foreach ($releases as $version => $url) + { + if ($selected_version == null) + { + $selected_version = $version; + continue; + } + + if (VersionComparator::compareVersion($version, $packageInput->Version) == 1) + $selected_version = $version; + } + + if ($selected_version == null) + throw new VersionNotFoundException('No releases found for the given repository.'); + } + else + { + $selected_version = $packageInput->Version; + } + + if (!isset($releases[$selected_version])) + throw new VersionNotFoundException(sprintf('No releases found for the given repository. (Selected version: %s)', $selected_version)); + + return $releases[$selected_version]; + } + } \ No newline at end of file diff --git a/src/ncc/Classes/GitlabExtension/GitlabService.php b/src/ncc/Classes/GitlabExtension/GitlabService.php new file mode 100644 index 0000000..3a4dd0e --- /dev/null +++ b/src/ncc/Classes/GitlabExtension/GitlabService.php @@ -0,0 +1,242 @@ +SSL ? "https" : "http"); + $owner_f = str_ireplace("/", "%2F", $packageInput->Vendor); + $owner_f = str_ireplace(".", "%2F", $owner_f); + $project_f = str_ireplace("/", "%2F", $packageInput->Package); + $project_f = str_ireplace(".", "%2F", $project_f); + $httpRequest->Url = $protocol . '://' . $definedRemoteSource->Host . "/api/v4/projects/$owner_f%2F$project_f"; + $httpRequest = Functions::prepareGitServiceRequest($httpRequest, $entry); + + $response = HttpClient::request($httpRequest, true); + + if($response->StatusCode != 200) + throw new GitlabServiceException(sprintf('Failed to fetch releases for the given repository. Status code: %s', $response->StatusCode)); + + $response_decoded = Functions::loadJson($response->Body, Functions::FORCE_ARRAY); + + $query = new RepositoryQueryResults(); + $query->Files->GitSshUrl = ($response_decoded['ssh_url_to_repo'] ?? null); + $query->Files->GitHttpUrl = ($response_decoded['http_url_to_repo'] ?? null); + $query->Version = Functions::convertToSemVer($response_decoded['default_branch']) ?? null; + $query->ReleaseDescription = ($response_decoded['description'] ?? null); + $query->ReleaseName = ($response_decoded['name'] ?? null); + + return $query; + } + + /** + * Returns the download URL of the requested version of the package. + * + * @param RemotePackageInput $packageInput + * @param DefinedRemoteSource $definedRemoteSource + * @param Entry|null $entry + * @return RepositoryQueryResults + * @throws AuthenticationException + * @throws GitlabServiceException + * @throws HttpException + * @throws MalformedJsonException + * @throws VersionNotFoundException + */ + public static function getRelease(RemotePackageInput $packageInput, DefinedRemoteSource $definedRemoteSource, ?Entry $entry = null): RepositoryQueryResults + { + $releases = self::getReleases($packageInput->Vendor, $packageInput->Package, $definedRemoteSource, $entry); + + if(count($releases) === 0) + throw new VersionNotFoundException('No releases found for the given repository.'); + + // Query the latest package only + if($packageInput->Version == Versions::Latest) + { + $latest_version = null; + foreach($releases as $release) + { + if($latest_version == null) + { + $latest_version = $release->Version; + continue; + } + + if(VersionComparator::compareVersion($release->Version, $latest_version) == 1) + $latest_version = $release->Version; + } + + return $releases[$latest_version]; + } + + // Query a specific version + if(!isset($releases[$packageInput->Version])) + { + // Find the closest thing to the requested version + $selected_version = null; + foreach($releases as $version => $url) + { + if($selected_version == null) + { + $selected_version = $version; + continue; + } + + if(VersionComparator::compareVersion($version, $packageInput->Version) == 1) + $selected_version = $version; + } + + if($selected_version == null) + throw new VersionNotFoundException('No releases found for the given repository.'); + } + else + { + $selected_version = $packageInput->Version; + } + + if(!isset($releases[$selected_version])) + throw new VersionNotFoundException(sprintf('No releases found for the given repository. (Selected version: %s)', $selected_version)); + + return $releases[$selected_version]; + } + + /** + * @param RemotePackageInput $packageInput + * @param DefinedRemoteSource $definedRemoteSource + * @param Entry|null $entry + * @return RepositoryQueryResults + * @throws NotSupportedException + */ + public static function getNccPackage(RemotePackageInput $packageInput, DefinedRemoteSource $definedRemoteSource, ?Entry $entry = null): RepositoryQueryResults + { + throw new NotSupportedException(sprintf('The given repository source "%s" does not support ncc packages.', $definedRemoteSource->Host)); + } + + /** + * Returns an array of all the tags for the given owner and repository name. + * + * @param string $owner + * @param string $repository + * @param DefinedRemoteSource $definedRemoteSource + * @param Entry|null $entry + * @return array + * @throws AuthenticationException + * @throws GitlabServiceException + * @throws HttpException + * @throws MalformedJsonException + */ + private static function getReleases(string $owner, string $repository, DefinedRemoteSource $definedRemoteSource, ?Entry $entry): array + { + $httpRequest = new HttpRequest(); + $protocol = ($definedRemoteSource->SSL ? "https" : "http"); + $owner_f = str_ireplace("/", "%2F", $owner); + $owner_f = str_ireplace(".", "%2F", $owner_f); + $repository_f = str_ireplace("/", "%2F", $repository); + $repository_f = str_ireplace(".", "%2F", $repository_f); + + $httpRequest->Url = $protocol . '://' . $definedRemoteSource->Host . "/api/v4/projects/$owner_f%2F$repository_f/releases"; + $httpRequest = Functions::prepareGitServiceRequest($httpRequest, $entry); + + $response = HttpClient::request($httpRequest, true); + + if($response->StatusCode != 200) + throw new GitlabServiceException(sprintf('Failed to fetch releases for the given repository. Status code: %s', $response->StatusCode)); + + $response_decoded = Functions::loadJson($response->Body, Functions::FORCE_ARRAY); + + if(count($response_decoded) == 0) + return []; + + $return = []; + foreach($response_decoded as $release) + { + $query_results = new RepositoryQueryResults(); + $query_results->ReleaseName = ($release['name'] ?? null); + $query_results->ReleaseDescription = ($release['description'] ?? null); + $query_results->Version = Functions::convertToSemVer($release['tag_name']); + + if(isset($release['assets']) && isset($release['assets']['sources'])) + { + if(count($release['assets']['sources']) > 0) + { + foreach($release['assets']['sources'] as $source) + { + if($source['format'] == 'zip') + { + $query_results->Files->ZipballUrl = $source['url']; + break; + } + + if($source['format'] == 'tar.gz') + { + $query_results->Files->ZipballUrl = $source['url']; + break; + } + + if($source['format'] == 'ncc') + { + $query_results->Files->PackageUrl = $source['url']; + break; + } + } + } + } + + $return[$query_results->Version] = $query_results; + } + + return $return; + } + } \ No newline at end of file diff --git a/src/ncc/Classes/HttpClient.php b/src/ncc/Classes/HttpClient.php new file mode 100644 index 0000000..11b47e9 --- /dev/null +++ b/src/ncc/Classes/HttpClient.php @@ -0,0 +1,241 @@ +Url); + curl_setopt($curl, CURLOPT_RETURNTRANSFER, true); + curl_setopt($curl, CURLOPT_HEADER, true); + curl_setopt($curl, CURLOPT_FOLLOWLOCATION, true); + curl_setopt($curl, CURLOPT_MAXREDIRS, 5); + curl_setopt($curl, CURLOPT_CONNECTTIMEOUT, 10); + curl_setopt($curl, CURLOPT_TIMEOUT, 10); + curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, false); + curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, false); + curl_setopt($curl, CURLOPT_HTTPHEADER, $request->Headers); + curl_setopt($curl, CURLOPT_CUSTOMREQUEST, $request->Type); + + switch($request->Type) + { + case HttpRequestType::GET: + curl_setopt($curl, CURLOPT_HTTPGET, true); + break; + + case HttpRequestType::POST: + curl_setopt($curl, CURLOPT_POST, true); + if($request->Body !== null) + curl_setopt($curl, CURLOPT_POSTFIELDS, $request->Body); + break; + + case HttpRequestType::PUT: + curl_setopt($curl, CURLOPT_CUSTOMREQUEST, 'PUT'); + if($request->Body !== null) + curl_setopt($curl, CURLOPT_POSTFIELDS, $request->Body); + break; + + case HttpRequestType::DELETE: + curl_setopt($curl, CURLOPT_CUSTOMREQUEST, 'DELETE'); + break; + } + + if (is_array($request->Authentication)) + { + curl_setopt($curl, CURLOPT_USERPWD, $request->Authentication[0] . ':' . $request->Authentication[1]); + } + else if (is_string($request->Authentication)) + { + curl_setopt($curl, CURLOPT_USERPWD, $request->Authentication); + } + + foreach ($request->Options as $option => $value) + curl_setopt($curl, $option, $value); + + return $curl; + } + + /** + * Creates a new HTTP request and returns the response. + * + * @param HttpRequest $httpRequest + * @param bool $cache + * @return HttpResponse + * @throws HttpException + */ + public static function request(HttpRequest $httpRequest, bool $cache=false): HttpResponse + { + if($cache) + { + /** @var HttpResponseCache $cache */ + $cache = RuntimeCache::get(sprintf('http_cache_%s', $httpRequest->requestHash())); + if($cache !== null && $cache->getTtl() > time()) + { + Console::outDebug(sprintf('using cached response for %s', $httpRequest->requestHash())); + return $cache->getHttpResponse(); + } + } + + $curl = self::prepareCurl($httpRequest); + + Console::outDebug(sprintf(' => %s request %s', $httpRequest->Type, $httpRequest->Url)); + if($httpRequest->Headers !== null && count($httpRequest->Headers) > 0) + Console::outDebug(sprintf(' => headers: %s', implode(', ', $httpRequest->Headers))); + if($httpRequest->Body !== null) + Console::outDebug(sprintf(' => body: %s', $httpRequest->Body)); + + $response = curl_exec($curl); + + if ($response === false) + { + $error = curl_error($curl); + curl_close($curl); + throw new HttpException($error); + } + + $headerSize = curl_getinfo($curl, CURLINFO_HEADER_SIZE); + $headers = substr($response, 0, $headerSize); + $body = substr($response, $headerSize); + + $httpResponse = new HttpResponse(); + $httpResponse->StatusCode = curl_getinfo($curl, CURLINFO_HTTP_CODE); + $httpResponse->Headers = self::parseHeaders($headers); + $httpResponse->Body = $body; + + Console::outDebug(sprintf(' <= %s response', $httpResponse->StatusCode));/** @noinspection PhpConditionAlreadyCheckedInspection */ + if($httpResponse->Headers !== null && count($httpResponse->Headers) > 0) + Console::outDebug(sprintf(' <= headers: %s', implode(', ', $httpResponse->Headers))); + /** @noinspection PhpConditionAlreadyCheckedInspection */ + if($httpResponse->Body !== null) + Console::outDebug(sprintf(' <= body: %s', $httpResponse->Body)); + + curl_close($curl); + + if($cache) + { + $httpCacheObject = new HttpResponseCache($httpResponse, time() + 60); + RuntimeCache::set(sprintf('http_cache_%s', $httpRequest->requestHash()), $httpCacheObject); + + Console::outDebug(sprintf('cached response for %s', $httpRequest->requestHash())); + } + + return $httpResponse; + } + + /** + * Downloads a file from the given url and saves it to the given path. + * + * @param HttpRequest $httpRequest + * @param string $path + * @return void + * @throws HttpException + */ + public static function download(HttpRequest $httpRequest, string $path): void + { + $curl = self::prepareCurl($httpRequest); + + $fp = fopen($path, 'w'); + curl_setopt($curl, CURLOPT_FILE, $fp); + curl_setopt($curl, CURLOPT_HEADER, 0); + $response = curl_exec($curl); + + if ($response === false) + { + $error = curl_error($curl); + curl_close($curl); + throw new HttpException($error); + } + + curl_close($curl); + fclose($fp); + } + + /** + * Displays the download progress in the console + * + * @param $downloadSize + * @param $downloaded + * @return void + */ + public static function displayProgress($downloadSize, $downloaded): void + { + if(Main::getLogLevel() !== null) + { + switch(Main::getLogLevel()) + { + case LogLevel::Verbose: + case LogLevel::Debug: + case LogLevel::Silent: + Console::outVerbose(sprintf(' <= %s of %s bytes downloaded', $downloaded, $downloadSize)); + break; + + default: + if ($downloadSize > 0) + Console::inlineProgressBar($downloaded, $downloadSize); + break; + } + } + + + } + + /** + * Takes the return headers of a cURL request and parses them into an array. + * + * @param string $input + * @return array + */ + private static function parseHeaders(string $input): array + { + $headers = array(); + $lines = explode("\n", $input); + $headers['HTTP'] = array_shift($lines); + + foreach ($lines as $line) { + $header = explode(':', $line, 2); + if (count($header) == 2) { + $headers[trim($header[0])] = trim($header[1]); + } + } + + return $headers; + } + } \ No newline at end of file diff --git a/src/ncc/Classes/LuaExtension/LuaRunner.php b/src/ncc/Classes/LuaExtension/LuaRunner.php new file mode 100644 index 0000000..599c5b2 --- /dev/null +++ b/src/ncc/Classes/LuaExtension/LuaRunner.php @@ -0,0 +1,53 @@ +Execute->Target = null; + $execution_unit->ExecutionPolicy = $policy; + $execution_unit->Data = IO::fread($path); + + return $execution_unit; + } + + /** + * @inheritDoc + */ + public static function getFileExtension(): string + { + return '.lua'; + } + } \ No newline at end of file diff --git a/src/ncc/Classes/NccExtension/ConstantCompiler.php b/src/ncc/Classes/NccExtension/ConstantCompiler.php index 5ab4597..5540632 100644 --- a/src/ncc/Classes/NccExtension/ConstantCompiler.php +++ b/src/ncc/Classes/NccExtension/ConstantCompiler.php @@ -1,14 +1,33 @@ ProjectType == ProjectType::Composer) + { + $project_path = ComposerSourceBuiltin::fromLocal($project_type->ProjectPath); + } + elseif($project_type->ProjectType == ProjectType::Ncc) + { + $project_manager = new ProjectManager($project_type->ProjectPath); + $project_manager->getProjectConfiguration()->Assembly->Version = $version; + $project_path = $project_manager->build(); + } + else + { + throw new UnsupportedProjectTypeException('The project type \'' . $project_type->ProjectType . '\' is not supported'); + } + + if($version !== null) + { + $package = Package::load($project_path); + $package->Assembly->Version = Functions::convertToSemVer($version); + $package->save($project_path); + } + + return $project_path; + } + catch(Exception $e) + { + throw new BuildException('Failed to build project', $e); + } + } + + /** * Compiles the execution policies of the package * @@ -93,7 +161,7 @@ * @throws AccessDeniedException * @throws FileNotFoundException * @throws IOException - * @throws UnsupportedRunnerException + * @throws RunnerExecutionException */ public static function compileExecutionPolicies(string $path, ProjectConfiguration $configuration): array { @@ -108,6 +176,8 @@ /** @var ProjectConfiguration\ExecutionPolicy $policy */ foreach($configuration->ExecutionPolicies as $policy) { + Console::outVerbose(sprintf('Compiling Execution Policy %s', $policy->Name)); + if($total_items > 5) { Console::inlineProgressBar($processed_items, $total_items); @@ -132,29 +202,29 @@ * @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 { + Console::outVerbose(sprintf('Writing package to %s', $path)); + // Write the package to disk $FileSystem = new Filesystem(); $BuildConfiguration = $configuration->Build->getBuildConfiguration($build_configuration); - if($FileSystem->exists($path . $BuildConfiguration->OutputPath)) + 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); - } + Console::outDebug(sprintf('creating output directory %s', $path . $BuildConfiguration->OutputPath)); + $FileSystem->mkdir($path . $BuildConfiguration->OutputPath); } // Finally write the package to the disk $FileSystem->mkdir($path . $BuildConfiguration->OutputPath); $output_file = $path . $BuildConfiguration->OutputPath . DIRECTORY_SEPARATOR . $package->Assembly->Package . '.ncc'; + if($FileSystem->exists($output_file)) + { + Console::outDebug(sprintf('removing existing package %s', $output_file)); + $FileSystem->remove($output_file); + } $FileSystem->touch($output_file); try @@ -169,35 +239,13 @@ 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 + * @noinspection PhpParameterByRefIsNotUsedAsReferenceInspection */ public static function compilePackageConstants(Package &$package, array $refs): void { @@ -206,6 +254,7 @@ $assembly = []; foreach($package->Assembly->toArray() as $key => $value) { + Console::outDebug(sprintf('compiling consts Assembly.%s (%s)', $key, implode(', ', array_keys($refs)))); $assembly[$key] = self::compileConstants($value, $refs); } $package->Assembly = Assembly::fromArray($assembly); @@ -217,11 +266,44 @@ $units = []; foreach($package->ExecutionUnits as $executionUnit) { + Console::outDebug(sprintf('compiling execution unit consts %s (%s)', $executionUnit->ExecutionPolicy->Name, implode(', ', array_keys($refs)))); $units[] = self::compileExecutionUnitConstants($executionUnit, $refs); } $package->ExecutionUnits = $units; unset($units); } + + $compiled_constants = []; + foreach($package->Header->RuntimeConstants as $name => $value) + { + Console::outDebug(sprintf('compiling runtime const %s (%s)', $name, implode(', ', array_keys($refs)))); + $compiled_constants[$name] = self::compileConstants($value, $refs); + } + + $options = []; + foreach($package->Header->Options as $name => $value) + { + if(is_array($value)) + { + $options[$name] = []; + foreach($value as $key => $val) + { + if(!is_string($val)) + continue; + + Console::outDebug(sprintf('compiling option %s.%s (%s)', $name, $key, implode(', ', array_keys($refs)))); + $options[$name][$key] = self::compileConstants($val, $refs); + } + } + else + { + Console::outDebug(sprintf('compiling option %s (%s)', $name, implode(', ', array_keys($refs)))); + $options[$name] = self::compileConstants((string)$value, $refs); + } + } + + $package->Header->Options = $options; + $package->Header->RuntimeConstants = $compiled_constants; } /** @@ -303,6 +385,9 @@ if(isset($refs[ConstantReferences::Install])) $value = ConstantCompiler::compileInstallConstants($value, $refs[ConstantReferences::Install]); + if(isset($refs[ConstantReferences::Runtime])) + $value = ConstantCompiler::compileRuntimeConstants($value); + return $value; } } \ No newline at end of file diff --git a/src/ncc/Classes/NccExtension/Runner.php b/src/ncc/Classes/NccExtension/Runner.php index 2133137..5a71e81 100644 --- a/src/ncc/Classes/NccExtension/Runner.php +++ b/src/ncc/Classes/NccExtension/Runner.php @@ -1,20 +1,35 @@ addUnit($package, $version, $unit, true); $ExecutionPointerManager->executeUnit($package, $version, $unit->ExecutionPolicy->Name); - $ExecutionPointerManager->cleanTemporaryUnits();; + $ExecutionPointerManager->cleanTemporaryUnits(); } } \ No newline at end of file diff --git a/src/ncc/Classes/PerlExtension/PerlRunner.php b/src/ncc/Classes/PerlExtension/PerlRunner.php new file mode 100644 index 0000000..46ee10d --- /dev/null +++ b/src/ncc/Classes/PerlExtension/PerlRunner.php @@ -0,0 +1,56 @@ +Execute->Target = null; + if(!file_exists($path) && !is_file($path)) + throw new FileNotFoundException($path); + $execution_unit->ExecutionPolicy = $policy; + $execution_unit->Data = IO::fread($path); + + return $execution_unit; + } + + /** + * @inheritDoc + */ + public static function getFileExtension(): string + { + return '.pl'; + } + } \ No newline at end of file diff --git a/src/ncc/Classes/PhpExtension/PhpCompiler.php b/src/ncc/Classes/PhpExtension/PhpCompiler.php index bb29ce9..bc5142b 100644 --- a/src/ncc/Classes/PhpExtension/PhpCompiler.php +++ b/src/ncc/Classes/PhpExtension/PhpCompiler.php @@ -1,4 +1,24 @@ package->Dependencies = $this->project->Build->Dependencies; $this->package->MainExecutionPolicy = $this->project->Build->Main; + // Add the option to create a symbolic link to the package + if(isset($this->project->Project->Options['create_symlink']) && $this->project->Project->Options['create_symlink'] === True) + $this->package->Header->Options['create_symlink'] = true; + // Add both the defined constants from the build configuration and the global constants. // Global constants are overridden $this->package->Header->RuntimeConstants = []; $this->package->Header->RuntimeConstants = array_merge( - ($selected_build_configuration?->DefineConstants ?? []), + ($selected_build_configuration->DefineConstants ?? []), ($this->project->Build->DefineConstants ?? []), ($this->package->Header->RuntimeConstants ?? []) ); @@ -105,6 +128,11 @@ $this->package->Header->CompilerVersion = NCC_VERSION_NUMBER; $this->package->Header->Options = $this->project->Project->Options; + if($this->project->Project->UpdateSource !== null) + { + $this->package->Header->UpdateSource = $this->project->Project->UpdateSource; + } + Console::outDebug('scanning project files'); Console::outDebug('theseer\DirectoryScanner - Copyright (c) 2009-2014 Arne Blankerts All rights reserved.'); @@ -128,67 +156,75 @@ // TODO: Re-implement the scanning process outside the compiler, as this is will be redundant // Scan for components first. - Console::outVerbose('Scanning for components... '); - /** @var SplFileInfo $item */ - /** @noinspection PhpRedundantOptionalArgumentInspection */ - foreach($DirectoryScanner($source_path, True) as $item) + + if(file_exists($source_path)) { - // Ignore directories, they're not important. :-) - if(is_dir($item->getPathName())) - continue; + Console::outVerbose('Scanning for components... '); + /** @var SplFileInfo $item */ + /** @noinspection PhpRedundantOptionalArgumentInspection */ + foreach($DirectoryScanner($source_path, True) as $item) + { + // Ignore directories, they're not important. :-) + if(is_dir($item->getPathName())) + continue; - $Component = new Package\Component(); - $Component->Name = Functions::removeBasename($item->getPathname(), $this->path); - $this->package->Components[] = $Component; + $Component = new Package\Component(); + $Component->Name = Functions::removeBasename($item->getPathname(), $this->path); + $this->package->Components[] = $Component; - Console::outVerbose(sprintf('Found component %s', $Component->Name)); - } + Console::outVerbose(sprintf('Found component %s', $Component->Name)); + } - if(count($this->package->Components) > 0) - { - Console::outVerbose(count($this->package->Components) . ' component(s) found'); + if(count($this->package->Components) > 0) + { + Console::outVerbose(count($this->package->Components) . ' component(s) found'); + } + else + { + Console::outVerbose('No components found'); + } + + // Clear previous excludes and includes + $DirectoryScanner->setExcludes(); + $DirectoryScanner->setIncludes(); + + // Ignore component files + if($selected_build_configuration->ExcludeFiles !== null && count($selected_build_configuration->ExcludeFiles) > 0) + { + $DirectoryScanner->setExcludes(array_merge($selected_build_configuration->ExcludeFiles, ComponentFileExtensions::Php)); + } + else + { + $DirectoryScanner->setExcludes(ComponentFileExtensions::Php); + } + + Console::outVerbose('Scanning for resources... '); + /** @var SplFileInfo $item */ + foreach($DirectoryScanner($source_path) as $item) + { + // Ignore directories, they're not important. :-) + if(is_dir($item->getPathName())) + continue; + + $Resource = new Package\Resource(); + $Resource->Name = Functions::removeBasename($item->getPathname(), $this->path); + $this->package->Resources[] = $Resource; + + Console::outVerbose(sprintf('found resource %s', $Resource->Name)); + } + + if(count($this->package->Resources) > 0) + { + Console::outVerbose(count($this->package->Resources) . ' resources(s) found'); + } + else + { + Console::outVerbose('No resources found'); + } } else { - Console::outVerbose('No components found'); - } - - // Clear previous excludes and includes - $DirectoryScanner->setExcludes([]); - $DirectoryScanner->setIncludes([]); - - // Ignore component files - if($selected_build_configuration->ExcludeFiles !== null && count($selected_build_configuration->ExcludeFiles) > 0) - { - $DirectoryScanner->setExcludes(array_merge($selected_build_configuration->ExcludeFiles, ComponentFileExtensions::Php)); - } - else - { - $DirectoryScanner->setExcludes(ComponentFileExtensions::Php); - } - - Console::outVerbose('Scanning for resources... '); - /** @var SplFileInfo $item */ - foreach($DirectoryScanner($source_path) as $item) - { - // Ignore directories, they're not important. :-) - if(is_dir($item->getPathName())) - continue; - - $Resource = new Package\Resource(); - $Resource->Name = Functions::removeBasename($item->getPathname(), $this->path); - $this->package->Resources[] = $Resource; - - Console::outVerbose(sprintf('found resource %s', $Resource->Name)); - } - - if(count($this->package->Resources) > 0) - { - Console::outVerbose(count($this->package->Resources) . ' resources(s) found'); - } - else - { - Console::outVerbose('No resources found'); + Console::outWarning('Source path does not exist, skipping resource and component scanning'); } $selected_dependencies = []; @@ -206,7 +242,6 @@ $lib_path = $selected_build_configuration->OutputPath . DIRECTORY_SEPARATOR . 'libs'; if($filesystem->exists($lib_path)) $filesystem->remove($lib_path); - $filesystem->mkdir($lib_path); Console::outVerbose('Scanning for dependencies... '); foreach($selected_dependencies as $dependency) @@ -222,6 +257,10 @@ $package = $package_lock_manager->getPackageLock()->getPackage($dependency->Name); $version = $package->getVersion($dependency->Version); Console::outDebug(sprintf('copying shadow package %s=%s to %s', $dependency->Name, $dependency->Version, $out_path)); + + if(!$filesystem->exists($lib_path)) + $filesystem->mkdir($lib_path); + $filesystem->copy($version->Location, $out_path); $dependency->Source = 'libs' . DIRECTORY_SEPARATOR . sprintf('%s=%s.lib', $dependency->Name, $dependency->Version); @@ -242,7 +281,7 @@ break; } - $this->package->Dependencies[] = $dependency; + $this->package->addDependency($dependency); } if(count($this->package->Dependencies) > 0) @@ -265,7 +304,6 @@ * @throws BuildException * @throws FileNotFoundException * @throws IOException - * @throws UnsupportedRunnerException */ public function build(): ?Package { @@ -404,11 +442,11 @@ * @throws AccessDeniedException * @throws FileNotFoundException * @throws IOException - * @throws UnsupportedRunnerException + * @throws RunnerExecutionException */ public function compileExecutionPolicies(): void { - PackageCompiler::compileExecutionPolicies($this->path, $this->project); + $this->package->ExecutionUnits = PackageCompiler::compileExecutionPolicies($this->path, $this->project); } /** diff --git a/src/ncc/Classes/PhpExtension/PhpInstaller.php b/src/ncc/Classes/PhpExtension/PhpInstaller.php index 611f21e..323df19 100644 --- a/src/ncc/Classes/PhpExtension/PhpInstaller.php +++ b/src/ncc/Classes/PhpExtension/PhpInstaller.php @@ -1,4 +1,24 @@ package->Header->Options !== null && isset($this->package->Header->Options['static_files'])) - { - $static_files = $this->package->Header->Options['static_files']; - $static_files_path = $installationPaths->getBinPath() . DIRECTORY_SEPARATOR . 'static_autoload.bin'; - - foreach($static_files as $file) - { - if(!file_exists($file)) - throw new InstallationException(sprintf('Static file %s does not exist', $file)); - } - - $static_files_exists = true; - IO::fwrite($static_files_path, ZiProto::encode($static_files)); - } - $autoload_path = $installationPaths->getBinPath() . DIRECTORY_SEPARATOR . 'autoload.php'; - $autoload_src = $this->generateAutoload($installationPaths->getSourcePath(), $autoload_path, $static_files_exists); + $autoload_src = $this->generateAutoload($installationPaths->getSourcePath(), $autoload_path); IO::fwrite($autoload_path, $autoload_src); } @@ -286,15 +288,13 @@ * * @param string $src * @param string $output - * @param bool $ignore_units * @return string * @throws AccessDeniedException * @throws CollectorException * @throws FileNotFoundException * @throws IOException - * @throws NoUnitsFoundException */ - private function generateAutoload(string $src, string $output, bool $ignore_units=false): string + private function generateAutoload(string $src, string $output): string { // Construct configuration $configuration = new Config([$src]); @@ -315,10 +315,6 @@ $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() && !$ignore_units) - { - throw new NoUnitsFoundException('No units were found in the project'); - } $template = IO::fread($configuration->getTemplate()); diff --git a/src/ncc/Classes/PhpExtension/PhpRunner.php b/src/ncc/Classes/PhpExtension/PhpRunner.php index 100a807..89f9898 100644 --- a/src/ncc/Classes/PhpExtension/PhpRunner.php +++ b/src/ncc/Classes/PhpExtension/PhpRunner.php @@ -1,18 +1,33 @@ Execute->Target = null; $execution_unit->ExecutionPolicy = $policy; - $execution_unit->Data = Base64::encode(IO::fread($target_file)); + $execution_unit->Data = IO::fread($path); return $execution_unit; } @@ -47,21 +61,4 @@ { 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/Classes/PhpExtension/PhpRuntime.php b/src/ncc/Classes/PhpExtension/PhpRuntime.php new file mode 100644 index 0000000..97d3df1 --- /dev/null +++ b/src/ncc/Classes/PhpExtension/PhpRuntime.php @@ -0,0 +1,124 @@ +getInstallPaths()->getBinPath() . DIRECTORY_SEPARATOR . 'autoload.php'; + $static_files = $versionEntry->getInstallPaths()->getBinPath() . DIRECTORY_SEPARATOR . 'static_autoload.bin'; + $constants_path = $versionEntry->getInstallPaths()->getDataPath() . DIRECTORY_SEPARATOR . 'const'; + $assembly_path = $versionEntry->getInstallPaths()->getDataPath() . DIRECTORY_SEPARATOR . 'assembly'; + + if(!file_exists($assembly_path)) + throw new ImportException('Cannot locate assembly file \'' . $assembly_path . '\''); + + try + { + $assembly_content = ZiProto::decode(IO::fread($assembly_path)); + $assembly = Assembly::fromArray($assembly_content); + } + catch(Exception $e) + { + throw new ImportException('Failed to load assembly file \'' . $assembly_path . '\': ' . $e->getMessage()); + } + + if(file_exists($constants_path)) + { + try + { + $constants = ZiProto::decode(IO::fread($constants_path)); + } + catch(Exception $e) + { + throw new ImportException('Failed to load constants file \'' . $constants_path . '\': ' . $e->getMessage()); + } + + foreach($constants as $name => $value) + { + $value = ConstantCompiler::compileRuntimeConstants($value); + + try + { + Constants::register($assembly->Package, $name, $value, true); + } + catch (ConstantReadonlyException $e) + { + trigger_error('Constant \'' . $name . '\' is readonly (' . $assembly->Package . ')', E_USER_WARNING); + } + catch (InvalidConstantNameException $e) + { + throw new ImportException('Invalid constant name \'' . $name . '\' (' . $assembly->Package . ')', $e); + } + } + } + + if(file_exists($autoload_path) && !in_array(RuntimeImportOptions::ImportAutoloader, $options)) + { + require_once($autoload_path); + } + + if(file_exists($static_files) && !in_array(RuntimeImportOptions::ImportStaticFiles, $options)) + { + try + { + $static_files = ZiProto::decode(IO::fread($static_files)); + foreach($static_files as $file) + require_once($file); + } + catch(Exception $e) + { + throw new ImportException('Failed to load static files: ' . $e->getMessage(), $e); + } + + } + + if(!file_exists($autoload_path) && !file_exists($static_files)) + return false; + + return true; + } + } \ No newline at end of file diff --git a/src/ncc/Classes/PythonExtension/Python2Runner.php b/src/ncc/Classes/PythonExtension/Python2Runner.php new file mode 100644 index 0000000..b411a06 --- /dev/null +++ b/src/ncc/Classes/PythonExtension/Python2Runner.php @@ -0,0 +1,56 @@ +Execute->Target = null; + $execution_unit->ExecutionPolicy = $policy; + $execution_unit->Data = IO::fread($path); + + return $execution_unit; + } + + /** + * @inheritDoc + */ + public static function getFileExtension(): string + { + return '.py'; + } + } \ No newline at end of file diff --git a/src/ncc/Classes/PythonExtension/Python3Runner.php b/src/ncc/Classes/PythonExtension/Python3Runner.php new file mode 100644 index 0000000..bc6ff39 --- /dev/null +++ b/src/ncc/Classes/PythonExtension/Python3Runner.php @@ -0,0 +1,56 @@ +Execute->Target = null; + $execution_unit->ExecutionPolicy = $policy; + $execution_unit->Data = IO::fread($path); + + return $execution_unit; + } + + /** + * @inheritDoc + */ + public static function getFileExtension(): string + { + return '.py'; + } + } \ No newline at end of file diff --git a/src/ncc/Classes/PythonExtension/PythonRunner.php b/src/ncc/Classes/PythonExtension/PythonRunner.php new file mode 100644 index 0000000..b3d5168 --- /dev/null +++ b/src/ncc/Classes/PythonExtension/PythonRunner.php @@ -0,0 +1,56 @@ +Execute->Target = null; + $execution_unit->ExecutionPolicy = $policy; + $execution_unit->Data = IO::fread($path); + + return $execution_unit; + } + + /** + * @inheritDoc + */ + public static function getFileExtension(): string + { + return '.py'; + } +} \ No newline at end of file diff --git a/src/ncc/Exceptions/AccessDeniedException.php b/src/ncc/Exceptions/AccessDeniedException.php index 96bb695..f1af713 100644 --- a/src/ncc/Exceptions/AccessDeniedException.php +++ b/src/ncc/Exceptions/AccessDeniedException.php @@ -1,6 +1,26 @@ message = $message; - $this->previous = $previous; } } \ No newline at end of file diff --git a/src/ncc/Exceptions/BuildException.php b/src/ncc/Exceptions/BuildException.php index 7fbb3d8..4394f96 100644 --- a/src/ncc/Exceptions/BuildException.php +++ b/src/ncc/Exceptions/BuildException.php @@ -1,8 +1,26 @@ message = $message; - $this->previous = $previous; } } \ No newline at end of file diff --git a/src/ncc/Exceptions/ComponentChecksumException.php b/src/ncc/Exceptions/ComponentChecksumException.php index 68bb520..9261aa7 100644 --- a/src/ncc/Exceptions/ComponentChecksumException.php +++ b/src/ncc/Exceptions/ComponentChecksumException.php @@ -1,8 +1,26 @@ 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 index 593255f..6d08524 100644 --- a/src/ncc/Exceptions/ComponentDecodeException.php +++ b/src/ncc/Exceptions/ComponentDecodeException.php @@ -1,8 +1,26 @@ message = $message; - $this->previous = $previous; } } \ No newline at end of file diff --git a/src/ncc/Exceptions/ComponentVersionNotFoundException.php b/src/ncc/Exceptions/ComponentVersionNotFoundException.php index ae57404..8edbc99 100644 --- a/src/ncc/Exceptions/ComponentVersionNotFoundException.php +++ b/src/ncc/Exceptions/ComponentVersionNotFoundException.php @@ -1,6 +1,26 @@ message = $message; - $this->previous = $previous; } } \ No newline at end of file diff --git a/src/ncc/Exceptions/ComposerException.php b/src/ncc/Exceptions/ComposerException.php index 9e37388..52bc4b7 100644 --- a/src/ncc/Exceptions/ComposerException.php +++ b/src/ncc/Exceptions/ComposerException.php @@ -1,8 +1,26 @@ message = $message; - $this->previous = $previous; } } \ No newline at end of file diff --git a/src/ncc/Exceptions/ComposerNotAvailableException.php b/src/ncc/Exceptions/ComposerNotAvailableException.php index 2fa1d4b..2f99a51 100644 --- a/src/ncc/Exceptions/ComposerNotAvailableException.php +++ b/src/ncc/Exceptions/ComposerNotAvailableException.php @@ -1,20 +1,32 @@ message = $message; - $this->previous = $previous; } } \ No newline at end of file diff --git a/src/ncc/Exceptions/ConstantReadonlyException.php b/src/ncc/Exceptions/ConstantReadonlyException.php index 5930827..9ccbeef 100644 --- a/src/ncc/Exceptions/ConstantReadonlyException.php +++ b/src/ncc/Exceptions/ConstantReadonlyException.php @@ -1,6 +1,26 @@ code = ExceptionCodes::DirectoryNotFoundException; } } \ No newline at end of file diff --git a/src/ncc/Exceptions/ExecutionUnitNotFoundException.php b/src/ncc/Exceptions/ExecutionUnitNotFoundException.php deleted file mode 100644 index 228f512..0000000 --- a/src/ncc/Exceptions/ExecutionUnitNotFoundException.php +++ /dev/null @@ -1,8 +0,0 @@ -code = ExceptionCodes::FileNotFoundException; } } \ No newline at end of file diff --git a/src/ncc/Exceptions/GitCheckoutException.php b/src/ncc/Exceptions/GitCheckoutException.php new file mode 100644 index 0000000..aaad3c4 --- /dev/null +++ b/src/ncc/Exceptions/GitCheckoutException.php @@ -0,0 +1,39 @@ +message = $message; - $this->previous = $previous; } } \ No newline at end of file diff --git a/src/ncc/Exceptions/ImportException.php b/src/ncc/Exceptions/ImportException.php new file mode 100644 index 0000000..3e99de3 --- /dev/null +++ b/src/ncc/Exceptions/ImportException.php @@ -0,0 +1,39 @@ +message = $message; - $this->previous = $previous; } } \ No newline at end of file diff --git a/src/ncc/Exceptions/InternalComposerNotAvailableException.php b/src/ncc/Exceptions/InternalComposerNotAvailableException.php index 718114b..1d4808c 100644 --- a/src/ncc/Exceptions/InternalComposerNotAvailableException.php +++ b/src/ncc/Exceptions/InternalComposerNotAvailableException.php @@ -1,8 +1,26 @@ message = $message; - $this->previous = $previous; } } \ No newline at end of file diff --git a/src/ncc/Exceptions/InvalidBuildConfigurationException.php b/src/ncc/Exceptions/InvalidBuildConfigurationException.php new file mode 100644 index 0000000..80cb3cc --- /dev/null +++ b/src/ncc/Exceptions/InvalidBuildConfigurationException.php @@ -0,0 +1,39 @@ +message = $message; - $this->previous = $previous; } } \ No newline at end of file diff --git a/src/ncc/Exceptions/InvalidCredentialsEntryException.php b/src/ncc/Exceptions/InvalidCredentialsEntryException.php index 22bf01c..5b0d6db 100644 --- a/src/ncc/Exceptions/InvalidCredentialsEntryException.php +++ b/src/ncc/Exceptions/InvalidCredentialsEntryException.php @@ -1,6 +1,26 @@ message = $message; - $this->previous = $previous; } } \ No newline at end of file diff --git a/src/ncc/Exceptions/InvalidPackageNameException.php b/src/ncc/Exceptions/InvalidPackageNameException.php index da9ebaa..fbaeb18 100644 --- a/src/ncc/Exceptions/InvalidPackageNameException.php +++ b/src/ncc/Exceptions/InvalidPackageNameException.php @@ -1,6 +1,26 @@ message = $message; - $this->previous = $previous; } } \ No newline at end of file diff --git a/src/ncc/Exceptions/InvalidProjectConfigurationException.php b/src/ncc/Exceptions/InvalidProjectConfigurationException.php index 4923365..a8ec2bf 100644 --- a/src/ncc/Exceptions/InvalidProjectConfigurationException.php +++ b/src/ncc/Exceptions/InvalidProjectConfigurationException.php @@ -1,6 +1,26 @@ message = $message; - $this->previous = $previous; $this->property = $property; } + + /** + * @return string|null + */ + public function getProperty(): ?string + { + return $this->property; + } } \ No newline at end of file diff --git a/src/ncc/Exceptions/InvalidProjectNameException.php b/src/ncc/Exceptions/InvalidProjectNameException.php index 8778bce..bf3adff 100644 --- a/src/ncc/Exceptions/InvalidProjectNameException.php +++ b/src/ncc/Exceptions/InvalidProjectNameException.php @@ -1,6 +1,26 @@ message = $message; - $this->previous = $previous; } } \ No newline at end of file diff --git a/src/ncc/Exceptions/InvalidVersionNumberException.php b/src/ncc/Exceptions/InvalidVersionNumberException.php index 708c16a..23b1e8f 100644 --- a/src/ncc/Exceptions/InvalidVersionNumberException.php +++ b/src/ncc/Exceptions/InvalidVersionNumberException.php @@ -1,6 +1,26 @@ message = $message; - $this->previous = $previous; } } \ No newline at end of file diff --git a/src/ncc/Exceptions/NoAvailableUnitsException.php b/src/ncc/Exceptions/NoAvailableUnitsException.php index 56d61ac..eb7f325 100644 --- a/src/ncc/Exceptions/NoAvailableUnitsException.php +++ b/src/ncc/Exceptions/NoAvailableUnitsException.php @@ -1,8 +1,26 @@ message = $message; - $this->previous = $previous; } } \ No newline at end of file diff --git a/src/ncc/Exceptions/NoUnitsFoundException.php b/src/ncc/Exceptions/NoUnitsFoundException.php index a59e3da..48423b7 100644 --- a/src/ncc/Exceptions/NoUnitsFoundException.php +++ b/src/ncc/Exceptions/NoUnitsFoundException.php @@ -1,6 +1,26 @@ message = $message; - $this->previous = $previous; } } \ No newline at end of file diff --git a/src/ncc/Exceptions/NotImplementedException.php b/src/ncc/Exceptions/NotImplementedException.php index e53d390..55de1d1 100644 --- a/src/ncc/Exceptions/NotImplementedException.php +++ b/src/ncc/Exceptions/NotImplementedException.php @@ -1,8 +1,26 @@ message = $message; - $this->previous = $previous; } } \ No newline at end of file diff --git a/src/ncc/Exceptions/NotSupportedException.php b/src/ncc/Exceptions/NotSupportedException.php new file mode 100644 index 0000000..a629e67 --- /dev/null +++ b/src/ncc/Exceptions/NotSupportedException.php @@ -0,0 +1,39 @@ +message = $message; - $this->previous = $previous; } } \ No newline at end of file diff --git a/src/ncc/Exceptions/PackageFetchException.php b/src/ncc/Exceptions/PackageFetchException.php new file mode 100644 index 0000000..4dbbbed --- /dev/null +++ b/src/ncc/Exceptions/PackageFetchException.php @@ -0,0 +1,39 @@ +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 index 88bd370..070d601 100644 --- a/src/ncc/Exceptions/PackageNotFoundException.php +++ b/src/ncc/Exceptions/PackageNotFoundException.php @@ -1,8 +1,26 @@ 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 index 70c1be7..7106d1a 100644 --- a/src/ncc/Exceptions/PackageParsingException.php +++ b/src/ncc/Exceptions/PackageParsingException.php @@ -1,8 +1,26 @@ message = $message; - $this->previous = $previous; } } \ No newline at end of file diff --git a/src/ncc/Exceptions/PackagePreparationFailedException.php b/src/ncc/Exceptions/PackagePreparationFailedException.php index 5a45e3e..223795f 100644 --- a/src/ncc/Exceptions/PackagePreparationFailedException.php +++ b/src/ncc/Exceptions/PackagePreparationFailedException.php @@ -1,8 +1,26 @@ message = $message; - $this->previous = $previous; } } \ No newline at end of file diff --git a/src/ncc/Exceptions/ProjectAlreadyExistsException.php b/src/ncc/Exceptions/ProjectAlreadyExistsException.php index b1d5f6c..3354002 100644 --- a/src/ncc/Exceptions/ProjectAlreadyExistsException.php +++ b/src/ncc/Exceptions/ProjectAlreadyExistsException.php @@ -1,6 +1,26 @@ 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 index e568dd1..244a0b5 100644 --- a/src/ncc/Exceptions/RunnerExecutionException.php +++ b/src/ncc/Exceptions/RunnerExecutionException.php @@ -1,8 +1,26 @@ message = $message; - $this->previous = $previous; } } \ No newline at end of file diff --git a/src/ncc/Exceptions/RuntimeException.php b/src/ncc/Exceptions/RuntimeException.php index a3ddb60..feb0953 100644 --- a/src/ncc/Exceptions/RuntimeException.php +++ b/src/ncc/Exceptions/RuntimeException.php @@ -1,6 +1,26 @@ message = $message; - $this->previous = $previous; } } \ No newline at end of file diff --git a/src/ncc/Exceptions/UnsupportedArchiveException.php b/src/ncc/Exceptions/UnsupportedArchiveException.php new file mode 100644 index 0000000..4a7bf33 --- /dev/null +++ b/src/ncc/Exceptions/UnsupportedArchiveException.php @@ -0,0 +1,39 @@ +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 index ecd4c79..5352a41 100644 --- a/src/ncc/Exceptions/UnsupportedComponentTypeException.php +++ b/src/ncc/Exceptions/UnsupportedComponentTypeException.php @@ -1,8 +1,26 @@ message = $message; - $this->previous = $previous; } } \ No newline at end of file diff --git a/src/ncc/Exceptions/UnsupportedExtensionVersionException.php b/src/ncc/Exceptions/UnsupportedExtensionVersionException.php index f24b429..4084580 100644 --- a/src/ncc/Exceptions/UnsupportedExtensionVersionException.php +++ b/src/ncc/Exceptions/UnsupportedExtensionVersionException.php @@ -1,13 +1,39 @@ message = $message; - $this->previous = $previous; } } \ No newline at end of file diff --git a/src/ncc/Exceptions/UnsupportedProjectTypeException.php b/src/ncc/Exceptions/UnsupportedProjectTypeException.php new file mode 100644 index 0000000..6cb7904 --- /dev/null +++ b/src/ncc/Exceptions/UnsupportedProjectTypeException.php @@ -0,0 +1,40 @@ +message = $message; - $this->previous = $previous; } } \ No newline at end of file diff --git a/src/ncc/Exceptions/VersionNotFoundException.php b/src/ncc/Exceptions/VersionNotFoundException.php index b7b4acc..c1348ea 100644 --- a/src/ncc/Exceptions/VersionNotFoundException.php +++ b/src/ncc/Exceptions/VersionNotFoundException.php @@ -1,8 +1,26 @@ message = $message; - $this->previous = $previous; } } \ No newline at end of file diff --git a/src/ncc/Extensions/ZiProto/Abstracts/Options.php b/src/ncc/Extensions/ZiProto/Abstracts/Options.php index 58534f0..40c0cdb 100644 --- a/src/ncc/Extensions/ZiProto/Abstracts/Options.php +++ b/src/ncc/Extensions/ZiProto/Abstracts/Options.php @@ -1,6 +1,26 @@ decodeStrData($this->decodeUint8()); + case 0xda: case 0xc5: return $this->decodeStrData($this->decodeUint16()); + case 0xdb: case 0xc6: return $this->decodeStrData($this->decodeUint32()); // float @@ -210,9 +234,6 @@ case 0xd3: return $this->decodeInt64(); // str - case 0xd9: return $this->decodeStrData($this->decodeUint8()); - case 0xda: return $this->decodeStrData($this->decodeUint16()); - case 0xdb: return $this->decodeStrData($this->decodeUint32()); // array case 0xdc: return $this->decodeArrayData($this->decodeUint16()); @@ -351,7 +372,7 @@ } /** - * @return bool|string + * @return string */ public function decodeStr() { @@ -387,7 +408,7 @@ } /** - * @return bool|string + * @return string */ public function decodeBin() { diff --git a/src/ncc/Extensions/ZiProto/DecodingOptions.php b/src/ncc/Extensions/ZiProto/DecodingOptions.php index d2fc7f8..347b2a1 100644 --- a/src/ncc/Extensions/ZiProto/DecodingOptions.php +++ b/src/ncc/Extensions/ZiProto/DecodingOptions.php @@ -1,5 +1,26 @@ bigIntMode = self::getSingleOption('bigint', $bitmask, + $self->bigIntMode = self::getSingleOption($bitmask, Options::BIGINT_AS_STR | Options::BIGINT_AS_GMP | Options::BIGINT_AS_EXCEPTION @@ -66,12 +87,11 @@ } /** - * @param string $name * @param int $bitmask * @param int $validBitmask * @return int */ - private static function getSingleOption(string $name, int $bitmask, int $validBitmask) : int + private static function getSingleOption(int $bitmask, int $validBitmask) : int { $option = $bitmask & $validBitmask; if ($option === ($option & -$option)) @@ -91,6 +111,6 @@ $validOptions[] = __CLASS__.'::'.$map[$i]; } - throw InvalidOptionException::outOfRange($name, $validOptions); + throw InvalidOptionException::outOfRange('bigint', $validOptions); } } diff --git a/src/ncc/Extensions/ZiProto/EncodingOptions.php b/src/ncc/Extensions/ZiProto/EncodingOptions.php index 2525641..c713ba4 100644 --- a/src/ncc/Extensions/ZiProto/EncodingOptions.php +++ b/src/ncc/Extensions/ZiProto/EncodingOptions.php @@ -1,6 +1,26 @@ = 0) { @@ -237,7 +257,7 @@ * @param $float * @return string */ - public function encodeFloat($float) + public function encodeFloat($float): string { return $this->isForceFloat32 ? "\xca". pack('G', $float) @@ -248,7 +268,7 @@ * @param $str * @return string */ - public function encodeStr($str) + public function encodeStr($str): string { $length = strlen($str); @@ -274,7 +294,7 @@ * @param $str * @return string */ - public function encodeBin($str) + public function encodeBin($str): string { $length = strlen($str); @@ -293,9 +313,9 @@ /** * @param $array - * @return false|string + * @return string */ - public function encodeArray($array) + public function encodeArray($array): string { $data = $this->encodeArrayHeader(count($array)); @@ -311,7 +331,7 @@ * @param $size * @return string */ - public function encodeArrayHeader($size) + public function encodeArrayHeader($size): string { if ($size <= 0xf) { @@ -328,9 +348,9 @@ /** * @param $map - * @return false|string + * @return string */ - public function encodeMap($map) + public function encodeMap($map): string { $data = $this->encodeMapHeader(count($map)); @@ -371,7 +391,7 @@ * @param $size * @return string */ - public function encodeMapHeader($size) + public function encodeMapHeader($size): string { if ($size <= 0xf) { @@ -391,7 +411,7 @@ * @param $data * @return string */ - public function encodeExt($type, $data) + public function encodeExt($type, $data): string { $length = strlen($data); diff --git a/src/ncc/Extensions/ZiProto/Type/Binary.php b/src/ncc/Extensions/ZiProto/Type/Binary.php index 2bb9078..bb8b85d 100644 --- a/src/ncc/Extensions/ZiProto/Type/Binary.php +++ b/src/ncc/Extensions/ZiProto/Type/Binary.php @@ -1,6 +1,26 @@ Configuration = RuntimeCache::get('ncc.yaml'); if($this->Configuration !== null) return; @@ -67,6 +89,8 @@ */ public function save(): void { + Console::outDebug(sprintf('saving configuration file to %s', PathFinder::getConfigurationFile())); + if(Resolver::resolveScope() !== Scopes::System) throw new AccessDeniedException('Cannot save configuration file, insufficient permissions'); @@ -88,6 +112,9 @@ */ public function getProperty(string $property) { + Console::outDebug(sprintf('getting property %s', $property)); + + Console::outDebug($property); $current_selection = $this->getConfiguration(); foreach(explode('.', strtolower($property)) as $property) { @@ -119,202 +146,20 @@ */ public function updateProperty(string $property, $value): bool { - // composer.options.quiet - $result = match (strtolower(explode('.', $property)[0])) - { - 'ncc' => $this->updateNccProperties($property, $value), - 'php' => $this->updatePhpProperties($property, $value), - 'git' => $this->updateGitProperties($property, $value), - 'runners' => $this->updateRunnerProperties($property, $value), - 'composer' => $this->updateComposerProperties($property, $value), - default => false, - }; + Console::outDebug(sprintf('updating property %s', $property)); + $keys = explode('.', $property); + $current = &$this->Configuration; + foreach ($keys as $k) + { + if (!array_key_exists($k, $current)) + { + return false; + } + $current = &$current[$k]; + } + $current = Functions::stringTypeCast($value); $this->save(); - return $result; - } - - /** - * Updates NCC configuration properties in the configuration - * - * @param string $property - * @param $value - * @return bool - */ - private function updateNccProperties(string $property, $value): bool - { - $delete = false; - if(is_null($value)) - $delete = true; - - switch(strtolower($property)) - { - case 'ncc.cli.no_colors': - $this->Configuration['ncc']['cli']['no_colors'] = Functions::cbool($value); - break; - case 'ncc.cli.basic_ascii': - $this->Configuration['ncc']['cli']['basic_ascii'] = Functions::cbool($value); - break; - case 'ncc.cli.logging': - $this->Configuration['ncc']['cli']['logging'] = ($delete ? LogLevel::Info : (string)$value); - break; - - default: - return false; - } - - return true; - } - - /** - * Updates PHP properties in the configuraiton - * - * @param string $property - * @param $value - * @return bool - */ - private function updatePhpProperties(string $property, $value): bool - { - $delete = false; - if(is_null($value)) - $delete = true; - - switch(strtolower($property)) - { - case 'php.executable_path': - $this->Configuration['php']['executable_path'] = ($delete ? null : (string)$value); - break; - case 'php.runtime.initialize_on_require': - $this->Configuration['php']['runtime']['initialize_on_require'] = Functions::cbool($value); - break; - case 'php.runtime.handle_exceptions': - $this->Configuration['php']['runtime']['handle_exceptions'] = Functions::cbool($value); - break; - - default: - return false; - } - - return true; - } - - /** - * Updated git properties - * - * @param string $property - * @param $value - * @return bool - */ - private function updateGitProperties(string $property, $value): bool - { - $delete = false; - if(is_null($value)) - $delete = true; - - switch(strtolower($property)) - { - case 'git.enabled': - $this->Configuration['git']['enabled'] = Functions::cbool($value); - break; - case 'git.executable_path': - $this->Configuration['git']['executable_path'] = ($delete? null : (string)$value); - break; - default: - return false; - } - - return true; - } - - /** - * Updaters runner properties - * - * @param string $property - * @param $value - * @return bool - */ - private function updateRunnerProperties(string $property, $value): bool - { - $delete = false; - if(is_null($value)) - $delete = true; - - switch(strtolower($property)) - { - case 'runners.php': - $this->Configuration['runners']['php'] = ($delete? null : (string)$value); - break; - case 'runners.bash': - $this->Configuration['runners']['bash'] = ($delete? null : (string)$value); - break; - case 'runners.sh': - $this->Configuration['runners']['sh'] = ($delete? null : (string)$value); - break; - case 'runners.python': - $this->Configuration['runners']['python'] = ($delete? null : (string)$value); - break; - case 'runners.python3': - $this->Configuration['runners']['python3'] = ($delete? null : (string)$value); - break; - case 'runners.python2': - $this->Configuration['runners']['python2'] = ($delete? null : (string)$value); - break; - - default: - return false; - } - - return true; - } - - /** - * Updates a composer property value - * - * @param string $property - * @param $value - * @return bool - */ - private function updateComposerProperties(string $property, $value): bool - { - $delete = false; - if(is_null($value)) - $delete = true; - - switch(strtolower($property)) - { - case 'composer.enabled': - $this->Configuration['composer']['enabled'] = Functions::cbool($value); - break; - case 'composer.enable_internal_composer': - $this->Configuration['composer']['enable_internal_composer'] = Functions::cbool($value); - break; - case 'composer.executable_path': - $this->Configuration['composer']['executable_path'] = ($delete? null : (string)$value); - break; - case 'composer.options.quiet': - $this->Configuration['composer']['options']['quiet'] = Functions::cbool($value); - break; - case 'composer.options.no_ansi': - $this->Configuration['composer']['options']['no_ansi'] = Functions::cbool($value); - break; - case 'composer.options.no_interaction': - $this->Configuration['composer']['options']['no_interaction'] = Functions::cbool($value); - break; - case 'composer.options.profile': - $this->Configuration['composer']['options']['profile'] = Functions::cbool($value); - break; - case 'composer.options.no_scripts': - $this->Configuration['composer']['options']['no_scripts'] = Functions::cbool($value); - break; - case 'composer.options.no_cache': - $this->Configuration['composer']['options']['no_cache'] = Functions::cbool($value); - break; - case 'composer.options.logging': - $this->Configuration['composer']['options']['logging'] = ((int)$value > 0 ? (int)$value : 1); - break; - default: - return false; - } return true; } diff --git a/src/ncc/Managers/CredentialManager.php b/src/ncc/Managers/CredentialManager.php index 4a9b2c2..679b14d 100644 --- a/src/ncc/Managers/CredentialManager.php +++ b/src/ncc/Managers/CredentialManager.php @@ -1,4 +1,24 @@ CredentialsPath = PathFinder::getDataPath(Scopes::System) . DIRECTORY_SEPARATOR . 'credentials.store'; - } + $this->Vault = null; - /** - * Determines if CredentialManager has correct access to manage credentials on the system - * - * @return bool - */ - public function checkAccess(): bool - { - $ResolvedScope = Resolver::resolveScope(); - - if($ResolvedScope !== Scopes::System) + try { - return False; + $this->loadVault(); + } + catch(Exception $e) + { + unset($e); } - return True; + if($this->Vault == null) + $this->Vault = new Vault(); } /** @@ -59,101 +82,86 @@ */ public function constructStore(): void { + Console::outDebug(sprintf('constructing credentials store at %s', $this->CredentialsPath)); + // Do not continue the function if the file already exists, if the file is damaged a separate function // is to be executed to fix the damaged file. if(file_exists($this->CredentialsPath)) return; - if(!$this->checkAccess()) - { + if(Resolver::resolveScope() !== Scopes::System) throw new AccessDeniedException('Cannot construct credentials store without system permissions'); - } $VaultObject = new Vault(); $VaultObject->Version = Versions::CredentialsStoreVersion; - IO::fwrite($this->CredentialsPath, ZiProto::encode($VaultObject->toArray()), 0600); + IO::fwrite($this->CredentialsPath, ZiProto::encode($VaultObject->toArray()), 0744); } /** - * Returns the vault object from the credentials store file. + * Loads the vault from the disk * - * @return Vault - * @throws AccessDeniedException - * @throws IOException - * @throws RuntimeException - */ - public function getVault(): Vault - { - $this->constructStore(); - - if(!$this->checkAccess()) - { - throw new AccessDeniedException('Cannot read credentials store without system permissions'); - } - - try - { - $Vault = ZiProto::decode(IO::fread($this->CredentialsPath)); - } - catch(Exception $e) - { - // TODO: Implement error-correction for corrupted credentials store. - throw new RuntimeException($e->getMessage(), $e); - } - - return Vault::fromArray($Vault); - } - - /** - * Saves the vault object to the credentials store - * - * @param Vault $vault * @return void * @throws AccessDeniedException * @throws IOException + * @throws RuntimeException + * @throws FileNotFoundException */ - public function saveVault(Vault $vault): void + private function loadVault(): void { - if(!$this->checkAccess()) + Console::outDebug(sprintf('loading credentials store from %s', $this->CredentialsPath)); + + if($this->Vault !== null) + return; + + if(!file_exists($this->CredentialsPath)) { - throw new AccessDeniedException('Cannot write to credentials store without system permissions'); + $this->Vault = new Vault(); + return; } - IO::fwrite($this->CredentialsPath, ZiProto::encode($vault->toArray()), 0600); + $VaultArray = ZiProto::decode(IO::fread($this->CredentialsPath)); + $VaultObject = Vault::fromArray($VaultArray); + + if($VaultObject->Version !== Versions::CredentialsStoreVersion) + throw new RuntimeException('Credentials store version mismatch'); + + $this->Vault = $VaultObject; } /** - * Registers an entry to the credentials store file + * Saves the vault to the disk * - * @param Vault\Entry $entry * @return void * @throws AccessDeniedException - * @throws InvalidCredentialsEntryException - * @throws RuntimeException * @throws IOException + * @noinspection PhpUnused */ - public function registerEntry(Vault\Entry $entry): void + public function saveVault(): void { - if(!preg_match('/^[\w-]+$/', $entry->Alias)) - { - throw new InvalidCredentialsEntryException('The property \'Alias\' must be alphanumeric (Regex error)'); - } + Console::outDebug(sprintf('saving credentials store to %s', $this->CredentialsPath)); - // TODO: Implement more validation checks for the rest of the entry properties. - // TODO: Implement encryption for entries that require encryption (For securing passwords and data) + if(Resolver::resolveScope() !== Scopes::System) + throw new AccessDeniedException('Cannot save credentials store without system permissions'); - $Vault = $this->getVault(); - $Vault->Entries[] = $entry; - - $this->saveVault($Vault); + IO::fwrite($this->CredentialsPath, ZiProto::encode($this->Vault->toArray()), 0744); } + /** - * @return null + * @return string + * @noinspection PhpUnused */ - public function getCredentialsPath(): ?string + public function getCredentialsPath(): string { return $this->CredentialsPath; } + + /** + * @return Vault|null + */ + public function getVault(): ?Vault + { + return $this->Vault; + } } \ No newline at end of file diff --git a/src/ncc/Managers/ExecutionPointerManager.php b/src/ncc/Managers/ExecutionPointerManager.php index c2f2dbb..9bedc3b 100644 --- a/src/ncc/Managers/ExecutionPointerManager.php +++ b/src/ncc/Managers/ExecutionPointerManager.php @@ -1,4 +1,24 @@ TemporaryUnits) == 0) return; + Console::outVerbose('Cleaning temporary units...'); + try { foreach($this->TemporaryUnits as $datum) { + Console::outDebug(sprintf('deleting unit %s=%s.%s', $datum['package'], $datum['version'], $datum['name'])); $this->removeUnit($datum['package'], $datum['version'], $datum['name']); } } @@ -100,9 +128,31 @@ */ private function getPackageId(string $package, string $version): string { + Console::outDebug(sprintf('calculating package id for %s=%s', $package, $version)); return hash('haval128,4', $package . $version); } + /** + * Returns the path to the execution pointer file + * + * @param string $package + * @param string $version + * @param string $name + * @return string + * @throws FileNotFoundException + */ + public function getEntryPointPath(string $package, string $version, string $name): string + { + $package_id = $this->getPackageId($package, $version); + $package_bin_path = $this->RunnerPath . DIRECTORY_SEPARATOR . $package_id; + $entry_point_path = $package_bin_path . DIRECTORY_SEPARATOR . hash('haval128,4', $name) . '.entrypoint'; + + if(!file_exists($entry_point_path)) + throw new FileNotFoundException('Cannot find entry point for ' . $package . '=' . $version . '.' . $name); + + return $entry_point_path; + } + /** * Adds a new Execution Unit to the * @@ -114,7 +164,7 @@ * @throws AccessDeniedException * @throws FileNotFoundException * @throws IOException - * @throws UnsupportedRunnerException + * @throws RunnerExecutionException * @noinspection PhpUnused */ public function addUnit(string $package, string $version, ExecutionUnit $unit, bool $temporary=false): void @@ -122,9 +172,17 @@ if(Resolver::resolveScope() !== Scopes::System) throw new AccessDeniedException('Cannot add new ExecutionUnit \'' . $unit->ExecutionPolicy->Name .'\' for ' . $package . ', insufficient permissions'); + Console::outVerbose(sprintf('Adding new ExecutionUnit \'%s\' for %s', $unit->ExecutionPolicy->Name, $package)); + $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; + $entry_point_path = $package_bin_path . DIRECTORY_SEPARATOR . hash('haval128,4', $unit->ExecutionPolicy->Name) . '.entrypoint'; + + Console::outDebug(sprintf('package_id=%s', $package_id)); + Console::outDebug(sprintf('package_config_path=%s', $package_config_path)); + Console::outDebug(sprintf('package_bin_path=%s', $package_bin_path)); + Console::outDebug(sprintf('entry_point_path=%s', $entry_point_path)); $filesystem = new Filesystem(); @@ -139,11 +197,20 @@ } $bin_file = $package_bin_path . DIRECTORY_SEPARATOR . hash('haval128,4', $unit->ExecutionPolicy->Name); - $bin_file .= match ($unit->ExecutionPolicy->Runner) { + $bin_file .= match ($unit->ExecutionPolicy->Runner) + { + Runners::bash => BashRunner::getFileExtension(), Runners::php => PhpRunner::getFileExtension(), - default => throw new UnsupportedRunnerException('The runner \'' . $unit->ExecutionPolicy->Runner . '\' is not supported'), + Runners::perl => PerlRunner::getFileExtension(), + Runners::python => PythonRunner::getFileExtension(), + Runners::python2 => Python2Runner::getFileExtension(), + Runners::python3 => Python3Runner::getFileExtension(), + Runners::lua => LuaRunner::getFileExtension(), + default => throw new RunnerExecutionException('The runner \'' . $unit->ExecutionPolicy->Runner . '\' is not supported'), }; + Console::outDebug(sprintf('bin_file=%s', $bin_file)); + if($filesystem->exists($bin_file) && $temporary) return; @@ -157,8 +224,19 @@ $execution_pointers->addUnit($unit, $bin_file); IO::fwrite($package_config_path, ZiProto::encode($execution_pointers->toArray(true))); + $entry_point = sprintf("#!%s\nncc exec --package=\"%s\" --exec-version=\"%s\" --exec-unit=\"%s\" --exec-args \"$@\"", + '/bin/bash', + $package, $version, $unit->ExecutionPolicy->Name + ); + + if(file_exists($entry_point_path)) + $filesystem->remove($entry_point_path); + IO::fwrite($entry_point_path, $entry_point); + chmod($entry_point_path, 0755); + if($temporary) { + Console::outVerbose(sprintf('Adding temporary ExecutionUnit \'%s\' for %s', $unit->ExecutionPolicy->Name, $package)); $this->TemporaryUnits[] = [ 'package' => $package, 'version' => $version, @@ -183,10 +261,16 @@ if(Resolver::resolveScope() !== Scopes::System) throw new AccessDeniedException('Cannot remove ExecutionUnit \'' . $name .'\' for ' . $package . ', insufficient permissions'); + Console::outVerbose(sprintf('Removing ExecutionUnit \'%s\' for %s', $name, $package)); + $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; + Console::outDebug(sprintf('package_id=%s', $package_id)); + Console::outDebug(sprintf('package_config_path=%s', $package_config_path)); + Console::outDebug(sprintf('package_bin_path=%s', $package_bin_path)); + $filesystem = new Filesystem(); if(!$filesystem->exists($package_config_path)) return false; @@ -225,16 +309,25 @@ */ public function getUnits(string $package, string $version): array { + Console::outVerbose(sprintf('getting execution units for %s', $package)); + $package_id = $this->getPackageId($package, $version); $package_config_path = $this->RunnerPath . DIRECTORY_SEPARATOR . $package_id . '.inx'; + Console::outDebug(sprintf('package_id=%s', $package_id)); + Console::outDebug(sprintf('package_config_path=%s', $package_config_path)); + if(!file_exists($package_config_path)) + { + Console::outWarning(sprintf('Path \'%s\' does not exist', $package_config_path)); return []; + } $execution_pointers = ExecutionPointers::fromArray(ZiProto::decode(IO::fread($package_config_path))); $results = []; foreach($execution_pointers->getPointers() as $pointer) { + Console::outDebug(sprintf('unit %s', $pointer->ExecutionPolicy->Name)); $results[] = $pointer->ExecutionPolicy->Name; } @@ -247,17 +340,18 @@ * @param string $package * @param string $version * @param string $name - * @return void + * @param array $args + * @return int * @throws AccessDeniedException - * @throws ExecutionUnitNotFoundException * @throws FileNotFoundException * @throws IOException * @throws NoAvailableUnitsException - * @throws UnsupportedRunnerException * @throws RunnerExecutionException */ - public function executeUnit(string $package, string $version, string $name): void + public function executeUnit(string $package, string $version, string $name, array $args=[]): int { + Console::outVerbose(sprintf('executing unit %s for %s', $name, $package)); + $package_id = $this->getPackageId($package, $version); $package_config_path = $this->RunnerPath . DIRECTORY_SEPARATOR . $package_id . '.inx'; @@ -268,18 +362,37 @@ $unit = $execution_pointers->getUnit($name); if($unit == null) - throw new ExecutionUnitNotFoundException('The execution unit \'' . $name . '\' was not found for \'' . $package . '=' .$version .'\''); + throw new RunnerExecutionException('The execution unit \'' . $name . '\' was not found for \'' . $package . '=' .$version .'\''); - $process = match (strtolower($unit->ExecutionPolicy->Runner)) + Console::outDebug(sprintf('unit=%s', $unit->ExecutionPolicy->Name)); + Console::outDebug(sprintf('runner=%s', $unit->ExecutionPolicy->Runner)); + Console::outDebug(sprintf('file=%s', $unit->FilePointer)); + Console::outDebug(sprintf('pass_thru_args=%s', json_encode($args, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE))); + + // Handle the arguments + if($unit->ExecutionPolicy->Execute->Options !== null && count($unit->ExecutionPolicy->Execute->Options) > 0) { - Runners::php => PhpRunner::prepareProcess($unit), - default => throw new UnsupportedRunnerException('The runner \'' . $unit->ExecutionPolicy->Runner . '\' is not supported'), - }; + $args = array_merge($args, $unit->ExecutionPolicy->Execute->Options); + + foreach($unit->ExecutionPolicy->Execute->Options as $option) + { + $args[] = ConstantCompiler::compileRuntimeConstants($option); + } + } + + $process = new Process(array_merge( + [PathFinder::findRunner(strtolower($unit->ExecutionPolicy->Runner)), $unit->FilePointer], $args) + ); if($unit->ExecutionPolicy->Execute->WorkingDirectory !== null) - $process->setWorkingDirectory($unit->ExecutionPolicy->Execute->WorkingDirectory); + { + $process->setWorkingDirectory(ConstantCompiler::compileRuntimeConstants($unit->ExecutionPolicy->Execute->WorkingDirectory)); + } + if($unit->ExecutionPolicy->Execute->Timeout !== null) + { $process->setTimeout((float)$unit->ExecutionPolicy->Execute->Timeout); + } if($unit->ExecutionPolicy->Execute->Silent) { @@ -296,12 +409,20 @@ $process->enableOutput(); } + Console::outDebug(sprintf('working_directory=%s', $process->getWorkingDirectory())); + Console::outDebug(sprintf('timeout=%s', ($process->getTimeout() ?? 0))); + Console::outDebug(sprintf('silent=%s', ($unit->ExecutionPolicy->Execute->Silent ? 'true' : 'false'))); + Console::outDebug(sprintf('tty=%s', ($unit->ExecutionPolicy->Execute->Tty ? 'true' : 'false'))); + Console::outDebug(sprintf('options=%s', implode(' ', $args))); + Console::outDebug(sprintf('cmd=%s', $process->getCommandLine())); + try { if($unit->ExecutionPolicy->Message !== null) Console::out($unit->ExecutionPolicy->Message); - $process->run(function ($type, $buffer) { + $process->run(function ($type, $buffer) + { Console::out($buffer); }); @@ -313,6 +434,8 @@ $this->handleExit($package, $version, $unit->ExecutionPolicy->ExitHandlers->Error); } + Console::outDebug(sprintf('exit_code=%s', $process->getExitCode())); + if($unit->ExecutionPolicy->ExitHandlers !== null) { if($process->isSuccessful() && $unit->ExecutionPolicy->ExitHandlers->Success !== null) @@ -330,6 +453,8 @@ $this->handleExit($package, $version, $unit->ExecutionPolicy->ExitHandlers->Error, $process); } } + + return $process->getExitCode(); } /** @@ -339,12 +464,10 @@ * @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 { @@ -391,12 +514,10 @@ * @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 { @@ -410,11 +531,13 @@ } elseif($exitHandle->EndProcess) { + Console::outDebug(sprintf('exit_code=%s', $process->getExitCode())); exit($exitHandle->ExitCode); } if($exitHandle->Run !== null) { + Console::outVerbose('Running unit \'' . $exitHandle->Run . '\''); $this->executeUnit($package, $version, $exitHandle->Run); } diff --git a/src/ncc/Managers/PackageLockManager.php b/src/ncc/Managers/PackageLockManager.php index 56d7daf..46134d6 100644 --- a/src/ncc/Managers/PackageLockManager.php +++ b/src/ncc/Managers/PackageLockManager.php @@ -1,7 +1,26 @@ sync(); + } + catch(Exception $e) + { + throw new PackageLockException('Failed to synchronize symlinks', $e); + } } /** diff --git a/src/ncc/Managers/PackageManager.php b/src/ncc/Managers/PackageManager.php index f22fafd..fb2cde0 100644 --- a/src/ncc/Managers/PackageManager.php +++ b/src/ncc/Managers/PackageManager.php @@ -1,4 +1,24 @@ 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) { + + $installer = match ($extension) + { CompilerExtensions::PHP => new PhpInstaller($package), default => throw new UnsupportedCompilerExtensionException('The compiler extension \'' . $extension . '\' is not supported'), }; + + if($this->getPackageVersion($package->Assembly->Package, $package->Assembly->Version) !== null) + { + if(in_array(InstallPackageOptions::Reinstall, $options)) + { + if($this->getPackageLockManager()->getPackageLock()->packageExists( + $package->Assembly->Package, $package->Assembly->Version + )) + { + $this->getPackageLockManager()->getPackageLock()->removePackageVersion( + $package->Assembly->Package, $package->Assembly->Version + ); + } + } + else + { + throw new PackageAlreadyInstalledException('The package ' . $package->Assembly->Package . '=' . $package->Assembly->Version . ' is already installed'); + } + } + $execution_pointer_manager = new ExecutionPointerManager(); PackageCompiler::compilePackageConstants($package, [ ConstantReferences::Install => $installation_paths ]); // Process all the required dependencies before installing the package - if($package->Dependencies !== null && count($package->Dependencies) > 0) + if($package->Dependencies !== null && count($package->Dependencies) > 0 && !in_array(InstallPackageOptions::SkipDependencies, $options)) { foreach($package->Dependencies as $dependency) { - $this->processDependency($dependency, $package, $package_path); + if(in_array(InstallPackageOptions::Reinstall, $options)) + { + // Uninstall the dependency if the option Reinstall is passed on + if($this->getPackageLockManager()->getPackageLock()->packageExists($dependency->Name, $dependency->Version)) + { + if($dependency->Version == null) + { + $this->uninstallPackage($dependency->Name); + } + else + { + $this->uninstallPackageVersion($dependency->Name, $dependency->Version); + } + } + } + + $this->processDependency($dependency, $package, $package_path, $entry); } } @@ -256,14 +330,15 @@ } // Install execution units - // TODO: Implement symlink support - if(count($package->ExecutionUnits) > 0) + if($package->ExecutionUnits !== null && count($package->ExecutionUnits) > 0) { $execution_pointer_manager = new ExecutionPointerManager(); $unit_paths = []; + /** @var Package\ExecutionUnit $executionUnit */ foreach($package->ExecutionUnits as $executionUnit) { + Console::outDebug(sprintf('processing execution unit %s', $executionUnit->ExecutionPolicy->Name)); $execution_pointer_manager->addUnit($package->Assembly->Package, $package->Assembly->Version, $executionUnit); $current_steps += 1; Console::inlineProgressBar($current_steps, $steps); @@ -272,6 +347,16 @@ IO::fwrite($installation_paths->getDataPath() . DIRECTORY_SEPARATOR . 'exec', ZiProto::encode($unit_paths)); } + // After execution units are installed, create a symlink if needed + if(isset($package->Header->Options['create_symlink']) && $package->Header->Options['create_symlink']) + { + if($package->MainExecutionPolicy === null) + throw new InstallationException('Cannot create symlink, no main execution policy is defined'); + + $SymlinkManager = new SymlinkManager(); + $SymlinkManager->add($package->Assembly->Package, $package->MainExecutionPolicy); + } + // Execute the post-installation stage after the installation is complete try { @@ -302,32 +387,227 @@ } } + if($package->Header->UpdateSource !== null && $package->Header->UpdateSource->Repository !== null) + { + $sources_manager = new RemoteSourcesManager(); + if($sources_manager->getRemoteSource($package->Header->UpdateSource->Repository->Name) === null) + { + Console::outVerbose('Adding remote source ' . $package->Header->UpdateSource->Repository->Name); + $defined_remote_source = new DefinedRemoteSource(); + $defined_remote_source->Name = $package->Header->UpdateSource->Repository->Name; + $defined_remote_source->Host = $package->Header->UpdateSource->Repository->Host; + $defined_remote_source->Type = $package->Header->UpdateSource->Repository->Type; + $defined_remote_source->SSL = $package->Header->UpdateSource->Repository->SSL; + + $sources_manager->addRemoteSource($defined_remote_source); + } + } + $this->getPackageLockManager()->getPackageLock()->addPackage($package, $installation_paths->getInstallationPath()); $this->getPackageLockManager()->save(); return $package->Assembly->Package; } + /** + * @param string $source + * @param Entry|null $entry + * @return string + * @throws InstallationException + * @throws NotImplementedException + * @throws PackageFetchException + */ + public function fetchFromSource(string $source, ?Entry $entry=null): string + { + $input = new RemotePackageInput($source); + + if($input->Source == null) + throw new PackageFetchException('No source specified'); + if($input->Package == null) + throw new PackageFetchException('No package specified'); + if($input->Version == null) + $input->Version = Versions::Latest; + + Console::outVerbose('Fetching package ' . $input->Package . ' from ' . $input->Source . ' (' . $input->Version . ')'); + + $remote_source_type = Resolver::detectRemoteSourceType($input->Source); + if($remote_source_type == RemoteSourceType::Builtin) + { + Console::outDebug('using builtin source ' . $input->Source); + switch($input->Source) + { + case 'composer': + try + { + return ComposerSourceBuiltin::fetch($input); + } + catch(Exception $e) + { + throw new PackageFetchException('Cannot fetch package from composer source, ' . $e->getMessage(), $e); + } + + default: + throw new NotImplementedException('Builtin source type ' . $input->Source . ' is not implemented'); + } + } + + if($remote_source_type == RemoteSourceType::Defined) + { + Console::outDebug('using defined source ' . $input->Source); + $remote_source_manager = new RemoteSourcesManager(); + $source = $remote_source_manager->getRemoteSource($input->Source); + if($source == null) + throw new InstallationException('Remote source ' . $input->Source . ' is not defined'); + + $repositoryQueryResults = Functions::getRepositoryQueryResults($input, $source, $entry); + $exceptions = []; + + if($repositoryQueryResults->Files->ZipballUrl !== null) + { + try + { + Console::outDebug(sprintf('fetching package %s from %s', $input->Package, $repositoryQueryResults->Files->ZipballUrl)); + $archive = Functions::downloadGitServiceFile($repositoryQueryResults->Files->ZipballUrl, $entry); + return PackageCompiler::tryCompile(Functions::extractArchive($archive), $repositoryQueryResults->Version); + } + catch(Throwable $e) + { + Console::outDebug('cannot fetch package from zipball url, ' . $e->getMessage()); + $exceptions[] = $e; + } + } + + if($repositoryQueryResults->Files->TarballUrl !== null) + { + try + { + Console::outDebug(sprintf('fetching package %s from %s', $input->Package, $repositoryQueryResults->Files->TarballUrl)); + $archive = Functions::downloadGitServiceFile($repositoryQueryResults->Files->TarballUrl, $entry); + return PackageCompiler::tryCompile(Functions::extractArchive($archive), $repositoryQueryResults->Version); + } + catch(Exception $e) + { + Console::outDebug('cannot fetch package from tarball url, ' . $e->getMessage()); + $exceptions[] = $e; + } + } + + if($repositoryQueryResults->Files->PackageUrl !== null) + { + try + { + Console::outDebug(sprintf('fetching package %s from %s', $input->Package, $repositoryQueryResults->Files->PackageUrl)); + return Functions::downloadGitServiceFile($repositoryQueryResults->Files->PackageUrl, $entry); + } + catch(Exception $e) + { + Console::outDebug('cannot fetch package from package url, ' . $e->getMessage()); + $exceptions[] = $e; + } + } + + if($repositoryQueryResults->Files->GitHttpUrl !== null || $repositoryQueryResults->Files->GitSshUrl !== null) + { + try + { + Console::outDebug(sprintf('fetching package %s from %s', $input->Package, $repositoryQueryResults->Files->GitHttpUrl ?? $repositoryQueryResults->Files->GitSshUrl)); + $git_repository = GitClient::cloneRepository($repositoryQueryResults->Files->GitHttpUrl ?? $repositoryQueryResults->Files->GitSshUrl); + + foreach(GitClient::getTags($git_repository) as $tag) + { + if(VersionComparator::compareVersion($tag, $repositoryQueryResults->Version) === 0) + { + GitClient::checkout($git_repository, $tag); + return PackageCompiler::tryCompile($git_repository, $repositoryQueryResults->Version); + } + } + + Console::outDebug('cannot fetch package from git repository, no matching tag found'); + } + catch(Exception $e) + { + Console::outDebug('cannot fetch package from git repository, ' . $e->getMessage()); + $exceptions[] = $e; + } + } + + // Recursively create an exception with the previous exceptions as the previous exception + $exception = null; + + if(count($exceptions) > 0) + { + foreach($exceptions as $e) + { + if($exception == null) + { + $exception = new PackageFetchException($e->getMessage(), $e); + } + else + { + if($e->getMessage() == $exception->getMessage()) + continue; + + $exception = new PackageFetchException($e->getMessage(), $exception); + } + } + } + else + { + $exception = new PackageFetchException('Cannot fetch package from remote source, no assets found'); + } + + throw $exception; + } + + throw new PackageFetchException(sprintf('Unknown remote source type %s', $remote_source_type)); + } + + /** + * Installs a package from a source syntax (vendor/package=version@source) + * + * @param string $source + * @param Entry|null $entry + * @return string + * @throws InstallationException + */ + public function installFromSource(string $source, ?Entry $entry): string + { + try + { + Console::outVerbose(sprintf('Installing package from source %s', $source)); + $package = $this->fetchFromSource($source, $entry); + return $this->install($package, $entry); + } + catch(Exception $e) + { + throw new InstallationException('Cannot install package from source, ' . $e->getMessage(), $e); + } + } + /** * @param Dependency $dependency * @param Package $package * @param string $package_path + * @param Entry|null $entry * @return void * @throws AccessDeniedException * @throws FileNotFoundException * @throws IOException * @throws InstallationException + * @throws InvalidPackageNameException + * @throws InvalidScopeException * @throws MissingDependencyException * @throws NotImplementedException * @throws PackageAlreadyInstalledException * @throws PackageLockException * @throws PackageNotFoundException * @throws PackageParsingException + * @throws SymlinkException * @throws UnsupportedCompilerExtensionException - * @throws UnsupportedRunnerException * @throws VersionNotFoundException + * @throws RunnerExecutionException */ - private function processDependency(Dependency $dependency, Package $package, string $package_path): void + private function processDependency(Dependency $dependency, Package $package, string $package_path, ?Entry $entry=null): void { Console::outVerbose('processing dependency ' . $dependency->Name . ' (' . $dependency->Version . ')'); $dependent_package = $this->getPackage($dependency->Name); @@ -335,39 +615,45 @@ if ($dependent_package !== null && $dependency->Version !== null && Validate::version($dependency->Version)) { + Console::outDebug('dependency has version constraint, checking if package is installed'); $dependent_version = $this->getPackageVersion($dependency->Name, $dependency->Version); if ($dependent_version !== null) $dependency_met = true; } elseif ($dependent_package !== null && $dependency->Version == null) { + Console::outDebug(sprintf('dependency %s has no version specified, assuming dependency is met', $dependency->Name)); $dependency_met = true; } - if ($dependency->SourceType !== null) + Console::outDebug('dependency met: ' . ($dependency_met ? 'true' : 'false')); + + if ($dependency->SourceType !== null && !$dependency_met) { - if(!$dependency_met) + Console::outVerbose(sprintf('Installing dependency %s=%s for %s=%s', $dependency->Name, $dependency->Version, $package->Assembly->Package, $package->Assembly->Version)); + switch ($dependency->SourceType) { - Console::outVerbose(sprintf('Installing dependency %s=%s for %s=%s', $dependency->Name, $dependency->Version, $package->Assembly->Package, $package->Assembly->Version)); - switch ($dependency->SourceType) - { - case DependencySourceType::Local: - Console::outDebug('installing from local source ' . $dependency->Source); - $basedir = dirname($package_path); - if (!file_exists($basedir . DIRECTORY_SEPARATOR . $dependency->Source)) - throw new FileNotFoundException($basedir . DIRECTORY_SEPARATOR . $dependency->Source); - $this->install($basedir . DIRECTORY_SEPARATOR . $dependency->Source); - break; + case DependencySourceType::Local: + Console::outDebug('installing from local source ' . $dependency->Source); + $basedir = dirname($package_path); + if (!file_exists($basedir . DIRECTORY_SEPARATOR . $dependency->Source)) + throw new FileNotFoundException($basedir . DIRECTORY_SEPARATOR . $dependency->Source); + $this->install($basedir . DIRECTORY_SEPARATOR . $dependency->Source); + break; - case DependencySourceType::StaticLinking: - throw new PackageNotFoundException('Static linking not possible, package ' . $dependency->Name . ' is not installed'); + case DependencySourceType::StaticLinking: + throw new PackageNotFoundException('Static linking not possible, package ' . $dependency->Name . ' is not installed'); - default: - throw new NotImplementedException('Dependency source type ' . $dependency->SourceType . ' is not implemented'); - } + case DependencySourceType::RemoteSource: + Console::outDebug('installing from remote source ' . $dependency->Source); + $this->installFromSource($dependency->Source, $entry); + break; + + default: + throw new NotImplementedException('Dependency source type ' . $dependency->SourceType . ' is not implemented'); } } - else + elseif(!$dependency_met) { throw new MissingDependencyException(sprintf('The dependency %s=%s for %s=%s is not met', $dependency->Name, $dependency->Version, $package->Assembly->Package, $package->Assembly->Version)); } @@ -379,10 +665,10 @@ * @param string $package * @return PackageEntry|null * @throws PackageLockException - * @throws PackageLockException */ public function getPackage(string $package): ?PackageEntry { + Console::outDebug('getting package ' . $package); return $this->getPackageLockManager()->getPackageLock()->getPackage($package); } @@ -397,6 +683,7 @@ */ public function getPackageVersion(string $package, string $version): ?VersionEntry { + Console::outDebug('getting package version ' . $package . '=' . $version); return $this->getPackage($package)?->getVersion($version); } @@ -407,9 +694,11 @@ * @return VersionEntry|null * @throws VersionNotFoundException * @throws PackageLockException + * @noinspection PhpUnused */ public function getLatestVersion(string $package): ?VersionEntry { + Console::outDebug('getting latest version of package ' . $package); return $this->getPackage($package)?->getVersion($this->getPackage($package)?->getLatestVersion()); } @@ -442,7 +731,15 @@ $exploded = explode('=', $package); try { - foreach ($this->getPackage($exploded[0])?->getVersion($exploded[1])?->Dependencies as $dependency) + $package = $this->getPackage($exploded[0]); + if($package == null) + throw new PackageNotFoundException('Package ' . $exploded[0] . ' not found'); + + $version = $package->getVersion($exploded[1]); + if($version == null) + throw new VersionNotFoundException('Version ' . $exploded[1] . ' not found for package ' . $exploded[0]); + + foreach ($version->Dependencies as $dependency) { if(!in_array($dependency->PackageName . '=' . $dependency->Version, $tree)) $packages[] = $dependency->PackageName . '=' . $dependency->Version; @@ -481,14 +778,21 @@ try { $version_entry = $this->getPackageVersion($package_e[0], $package_e[1]); - $tree[$package] = null; - if($version_entry->Dependencies !== null && count($version_entry->Dependencies) > 0) + if($version_entry == null) { - $tree[$package] = []; - foreach($version_entry->Dependencies as $dependency) + Console::outWarning('Version ' . $package_e[1] . ' of package ' . $package_e[0] . ' not found'); + } + else + { + $tree[$package] = null; + if($version_entry->Dependencies !== null && count($version_entry->Dependencies) > 0) { - $dependency_name = sprintf('%s=%s', $dependency->PackageName, $dependency->Version); - $tree[$package] = $this->getPackageTree($tree[$package], $dependency_name); + $tree[$package] = []; + foreach($version_entry->Dependencies as $dependency) + { + $dependency_name = sprintf('%s=%s', $dependency->PackageName, $dependency->Version); + $tree[$package] = $this->getPackageTree($tree[$package], $dependency_name); + } } } } @@ -512,6 +816,7 @@ * @throws IOException * @throws PackageLockException * @throws PackageNotFoundException + * @throws SymlinkException * @throws VersionNotFoundException */ public function uninstallPackageVersion(string $package, string $version): void @@ -534,16 +839,26 @@ $scanner = new DirectoryScanner(); $filesystem = new Filesystem(); - /** @var SplFileInfo $item */ - /** @noinspection PhpRedundantOptionalArgumentInspection */ - foreach($scanner($version_entry->Location, true) as $item) + if($filesystem->exists($version_entry->Location)) { - if(is_file($item->getPath())) + Console::outVerbose(sprintf('Removing package files from %s', $version_entry->Location)); + + /** @var SplFileInfo $item */ + /** @noinspection PhpRedundantOptionalArgumentInspection */ + foreach($scanner($version_entry->Location, true) as $item) { - Console::outDebug(sprintf('deleting %s', $item->getPath())); - $filesystem->remove($item->getPath()); + if(is_file($item->getPath())) + { + Console::outDebug('removing file ' . $item->getPath()); + Console::outDebug(sprintf('deleting %s', $item->getPath())); + $filesystem->remove($item->getPath()); + } } } + else + { + Console::outWarning(sprintf('warning: package location %s does not exist', $version_entry->Location)); + } $filesystem->remove($version_entry->Location); @@ -558,6 +873,9 @@ Console::outDebug(sprintf('warning: removing execution unit %s failed', $executionUnit->ExecutionPolicy->Name)); } } + + $symlink_manager = new SymlinkManager(); + $symlink_manager->sync(); } /** @@ -582,6 +900,7 @@ foreach($package_entry->getVersions() as $version) { $version_entry = $package_entry->getVersion($version); + try { $this->uninstallPackageVersion($package, $version_entry->Version); @@ -600,6 +919,8 @@ */ private static function initData(Package $package, InstallationPaths $paths): void { + Console::outVerbose(sprintf('Initializing data for %s', $package->Assembly->Name)); + // Create data files $dependencies = []; foreach($package->Dependencies as $dependency) @@ -611,7 +932,7 @@ $paths->getDataPath() . DIRECTORY_SEPARATOR . 'assembly' => ZiProto::encode($package->Assembly->toArray(true)), $paths->getDataPath() . DIRECTORY_SEPARATOR . 'ext' => - ZiProto::encode($package->Header->CompilerExtension->toArray(true)), + ZiProto::encode($package->Header->CompilerExtension->toArray()), $paths->getDataPath() . DIRECTORY_SEPARATOR . 'const' => ZiProto::encode($package->Header->RuntimeConstants), $paths->getDataPath() . DIRECTORY_SEPARATOR . 'dependencies' => @@ -622,6 +943,7 @@ { try { + Console::outDebug(sprintf('generating data file %s', $file)); IO::fwrite($file, $data); } catch (IOException $e) diff --git a/src/ncc/Managers/ProjectManager.php b/src/ncc/Managers/ProjectManager.php index 286910b..9e28ba6 100644 --- a/src/ncc/Managers/ProjectManager.php +++ b/src/ncc/Managers/ProjectManager.php @@ -1,4 +1,24 @@ ProjectConfiguration->Build->DefineConstants['ASSEMBLY_UID'] = '%ASSEMBLY.UID%'; // Generate configurations - $DebugConfiguration = new ProjectConfiguration\BuildConfiguration(); + $DebugConfiguration = new ProjectConfiguration\Build\BuildConfiguration(); $DebugConfiguration->Name = 'debug'; $DebugConfiguration->OutputPath = 'build/debug'; $DebugConfiguration->DefineConstants["DEBUG"] = '1'; // Debugging constant if the program wishes to check for this $this->ProjectConfiguration->Build->Configurations[] = $DebugConfiguration; - $ReleaseConfiguration = new ProjectConfiguration\BuildConfiguration(); + $ReleaseConfiguration = new ProjectConfiguration\Build\BuildConfiguration(); $ReleaseConfiguration->Name = 'release'; $ReleaseConfiguration->OutputPath = 'build/release'; $ReleaseConfiguration->DefineConstants["DEBUG"] = '0'; // Debugging constant if the program wishes to check for this @@ -172,14 +191,10 @@ // Process options foreach($options as $option) { - switch($option) - { - case InitializeProjectOptions::CREATE_SOURCE_DIRECTORY: - if(!file_exists($this->ProjectConfiguration->Build->SourcePath)) - { - mkdir($this->ProjectConfiguration->Build->SourcePath); - } - break; + if ($option == InitializeProjectOptions::CREATE_SOURCE_DIRECTORY) { + if (!file_exists($this->ProjectConfiguration->Build->SourcePath)) { + mkdir($this->ProjectConfiguration->Build->SourcePath); + } } } } @@ -207,7 +222,7 @@ * @throws FileNotFoundException * @throws IOException */ - public function load() + public function load(): void { if(!file_exists($this->ProjectFilePath) && !is_file($this->ProjectFilePath)) throw new ProjectConfigurationNotFoundException('The project configuration file \'' . $this->ProjectFilePath . '\' was not found'); @@ -221,7 +236,7 @@ * @return void * @throws MalformedJsonException */ - public function save() + public function save(): void { if(!$this->projectLoaded()) return; @@ -274,7 +289,6 @@ * @throws BuildException * @throws PackagePreparationFailedException * @throws UnsupportedCompilerExtensionException - * @throws UnsupportedRunnerException */ public function build(string $build_configuration=BuildConfigurationValues::DefaultConfiguration): string { diff --git a/src/ncc/Managers/RemoteSourcesManager.php b/src/ncc/Managers/RemoteSourcesManager.php new file mode 100644 index 0000000..dc1f884 --- /dev/null +++ b/src/ncc/Managers/RemoteSourcesManager.php @@ -0,0 +1,171 @@ +DefinedSourcesPath = PathFinder::getRemoteSources(Scopes::System); + + $this->load(); + } + + /** + * Loads an existing remote sources file, or creates a new one if it doesn't exist + * + * @return void + */ + public function load(): void + { + $this->Sources = []; + + try + { + + if(file_exists($this->DefinedSourcesPath)) + { + $sources = ZiProto::decode(IO::fread($this->DefinedSourcesPath)); + $this->Sources = []; + foreach($sources as $source) + $this->Sources[] = DefinedRemoteSource::fromArray($source); + } + } + catch(Exception $e) + { + unset($e); + } + } + + /** + * Saves the remote sources file to disk + * + * @return void + * @throws IOException + */ + public function save(): void + { + $sources = []; + foreach($this->Sources as $source) + $sources[] = $source->toArray(true); + + IO::fwrite($this->DefinedSourcesPath, ZiProto::encode($sources)); + } + + /** + * Adds a new remote source to the list + * + * @param DefinedRemoteSource $source + * @return bool + */ + public function addRemoteSource(DefinedRemoteSource $source): bool + { + foreach($this->Sources as $existingSource) + { + if($existingSource->Name === $source->Name) + return false; + } + + $this->Sources[] = $source; + return true; + } + + /** + * Gets a remote source by its name + * + * @param string $name + * @return DefinedRemoteSource|null + */ + public function getRemoteSource(string $name): ?DefinedRemoteSource + { + foreach($this->Sources as $source) + { + if($source->Name === $name) + return $source; + } + + return null; + } + + /** + * Deletes an existing remote source + * + * @param string $name + * @return bool + */ + public function deleteRemoteSource(string $name): bool + { + foreach($this->Sources as $index => $source) + { + if($source->Name === $name) + { + unset($this->Sources[$index]); + return true; + } + } + + return false; + } + + /** + * Returns an array of all the defined remote sources + * + * @return DefinedRemoteSource[] + */ + public function getSources(): array + { + if($this->Sources == null) + $this->load(); + return $this->Sources; + } + } \ No newline at end of file diff --git a/src/ncc/Managers/SymlinkManager.php b/src/ncc/Managers/SymlinkManager.php new file mode 100644 index 0000000..ce3e2e1 --- /dev/null +++ b/src/ncc/Managers/SymlinkManager.php @@ -0,0 +1,355 @@ +SymlinkDictionaryPath = PathFinder::getSymlinkDictionary(Scopes::System); + $this->load(); + } + catch(Exception $e) + { + Console::outWarning(sprintf('failed to load symlink dictionary from %s', $this->SymlinkDictionaryPath)); + } + finally + { + if($this->SymlinkDictionary === null) + $this->SymlinkDictionary = []; + + unset($e); + } + } + + /** + * Loads the symlink dictionary from the file + * + * @return void + * @throws AccessDeniedException + * @throws SymlinkException + */ + public function load(): void + { + if($this->SymlinkDictionary !== null) + return; + + Console::outDebug(sprintf('loading symlink dictionary from %s', $this->SymlinkDictionaryPath)); + + if(!file_exists($this->SymlinkDictionaryPath)) + { + Console::outDebug('symlink dictionary does not exist, creating new dictionary'); + $this->SymlinkDictionary = []; + $this->save(false); + return; + } + + try + { + $this->SymlinkDictionary = []; + + foreach(ZiProto::decode(IO::fread($this->SymlinkDictionaryPath)) as $entry) + { + $this->SymlinkDictionary[] = SymlinkEntry::fromArray($entry); + } + } + catch(Exception $e) + { + $this->SymlinkDictionary = []; + + Console::outDebug('symlink dictionary is corrupted, creating new dictionary'); + $this->save(false); + } + finally + { + unset($e); + } + } + + /** + * Saves the symlink dictionary to the file + * + * @param bool $throw_exception + * @return void + * @throws AccessDeniedException + * @throws SymlinkException + */ + private function save(bool $throw_exception=true): void + { + if(Resolver::resolveScope() !== Scopes::System) + throw new AccessDeniedException('Insufficient Permissions to write to the system symlink dictionary'); + + Console::outDebug(sprintf('saving symlink dictionary to %s', $this->SymlinkDictionaryPath)); + + try + { + $dictionary = []; + foreach($this->SymlinkDictionary as $entry) + { + $dictionary[] = $entry->toArray(true); + } + + IO::fwrite($this->SymlinkDictionaryPath, ZiProto::encode($dictionary)); + } + catch(Exception $e) + { + if($throw_exception) + throw new SymlinkException(sprintf('failed to save symlink dictionary to %s', $this->SymlinkDictionaryPath), $e); + + Console::outWarning(sprintf('failed to save symlink dictionary to %s', $this->SymlinkDictionaryPath)); + } + finally + { + unset($e); + } + } + + /** + * @return string + */ + public function getSymlinkDictionaryPath(): string + { + return $this->SymlinkDictionaryPath; + } + + /** + * @return array + */ + public function getSymlinkDictionary(): array + { + return $this->SymlinkDictionary; + } + + /** + * Checks if a package is defined in the symlink dictionary + * + * @param string $package + * @return bool + */ + public function exists(string $package): bool + { + foreach($this->SymlinkDictionary as $entry) + { + if($entry->Package === $package) + return true; + } + + return false; + } + + /** + * Adds a new entry to the symlink dictionary + * + * @param string $package + * @param string $unit + * @return void + * @throws AccessDeniedException + * @throws SymlinkException + */ + public function add(string $package, string $unit='main'): void + { + if(Resolver::resolveScope() !== Scopes::System) + throw new AccessDeniedException('Insufficient Permissions to add to the system symlink dictionary'); + + if($this->exists($package)) + $this->remove($package); + + $entry = new SymlinkEntry(); + $entry->Package = $package; + $entry->ExecutionPolicyName = $unit; + + $this->SymlinkDictionary[] = $entry; + $this->save(); + } + + /** + * Removes an entry from the symlink dictionary + * + * @param string $package + * @return void + * @throws AccessDeniedException + * @throws SymlinkException + */ + public function remove(string $package): void + { + if(Resolver::resolveScope() !== Scopes::System) + throw new AccessDeniedException('Insufficient Permissions to remove from the system symlink dictionary'); + + if(!$this->exists($package)) + return; + + foreach($this->SymlinkDictionary as $key => $entry) + { + if($entry->Package === $package) + { + if($entry->Registered) + { + $filesystem = new Filesystem(); + + $symlink_name = explode('.', $entry->Package)[count(explode('.', $entry->Package)) - 1]; + $symlink = self::$BinPath . DIRECTORY_SEPARATOR . $symlink_name; + + if($filesystem->exists($symlink)) + $filesystem->remove($symlink); + } + + unset($this->SymlinkDictionary[$key]); + $this->save(); + return; + } + } + + throw new SymlinkException(sprintf('failed to remove package %s from the symlink dictionary', $package)); + } + + /** + * Sets the package as registered + * + * @param string $package + * @return void + * @throws AccessDeniedException + * @throws SymlinkException + */ + private function setAsRegistered(string $package): void + { + foreach($this->SymlinkDictionary as $key => $entry) + { + if($entry->Package === $package) + { + $entry->Registered = true; + $this->SymlinkDictionary[$key] = $entry; + $this->save(); + return; + } + } + } + + /** + * Syncs the symlink dictionary with the filesystem + * + * @return void + * @throws AccessDeniedException + * @throws SymlinkException + */ + public function sync(): void + { + if(Resolver::resolveScope() !== Scopes::System) + throw new AccessDeniedException('Insufficient Permissions to sync the system symlink dictionary'); + + $filesystem = new Filesystem(); + $execution_pointer_manager = new ExecutionPointerManager(); + $package_lock_manager = new PackageLockManager(); + + foreach($this->SymlinkDictionary as $entry) + { + if($entry->Registered) + continue; + + $symlink_name = explode('.', $entry->Package)[count(explode('.', $entry->Package)) - 1]; + $symlink = self::$BinPath . DIRECTORY_SEPARATOR . $symlink_name; + + if($filesystem->exists($symlink)) + { + Console::outWarning(sprintf('Symlink %s already exists, skipping', $symlink)); + continue; + } + + try + { + $package_entry = $package_lock_manager->getPackageLock()->getPackage($entry->Package); + + if($package_entry == null) + { + Console::outWarning(sprintf('Package %s is not installed, skipping', $entry->Package)); + continue; + } + + $latest_version = $package_entry->getLatestVersion(); + + } + catch(Exception $e) + { + $filesystem->remove($symlink); + Console::outWarning(sprintf('Failed to get package %s, skipping', $entry->Package)); + continue; + } + + try + { + $entry_point_path = $execution_pointer_manager->getEntryPointPath($entry->Package, $latest_version, $entry->ExecutionPolicyName); + $filesystem->symlink($entry_point_path, $symlink); + } + catch(Exception $e) + { + $filesystem->remove($symlink); + Console::outWarning(sprintf('Failed to create symlink %s, skipping', $symlink)); + continue; + } + finally + { + unset($e); + } + + $this->setAsRegistered($entry->Package); + + } + } + } \ No newline at end of file diff --git a/src/ncc/Objects/CliHelpSection.php b/src/ncc/Objects/CliHelpSection.php index 90c878e..c8913a5 100644 --- a/src/ncc/Objects/CliHelpSection.php +++ b/src/ncc/Objects/CliHelpSection.php @@ -1,4 +1,24 @@ Readonly == true) + if($this->Readonly) { throw new ConstantReadonlyException('Cannot set value to the constant \'' . $this->getFullName() . '\', constant is readonly'); } diff --git a/src/ncc/Objects/DefinedRemoteSource.php b/src/ncc/Objects/DefinedRemoteSource.php new file mode 100644 index 0000000..de63655 --- /dev/null +++ b/src/ncc/Objects/DefinedRemoteSource.php @@ -0,0 +1,95 @@ + $this->Name, + ($bytecode ? Functions::cbc('type') : 'type') => $this->Type, + ($bytecode ? Functions::cbc('host') : 'host') => $this->Host, + ($bytecode ? Functions::cbc('ssl') : 'ssl') => $this->SSL + ]; + } + + /** + * Constructs object from an array representation. + * + * @param array $data + * @return static + */ + public static function fromArray(array $data): self + { + $definedRemoteSource = new self(); + + $definedRemoteSource->Name = Functions::array_bc($data, 'name'); + $definedRemoteSource->Type = Functions::array_bc($data, 'type'); + $definedRemoteSource->Host = Functions::array_bc($data, 'host'); + $definedRemoteSource->SSL = Functions::array_bc($data, 'ssl'); + + return $definedRemoteSource; + } + } \ No newline at end of file diff --git a/src/ncc/Objects/ExecutionPointers.php b/src/ncc/Objects/ExecutionPointers.php index 9181c03..c1f6a3b 100644 --- a/src/ncc/Objects/ExecutionPointers.php +++ b/src/ncc/Objects/ExecutionPointers.php @@ -1,4 +1,24 @@ Pointers as $pointer) { if($pointer->ExecutionPolicy->Name == $name) @@ -148,7 +172,11 @@ { $pointers[] = $pointer->toArray($bytecode); } - return $pointers; + return [ + ($bytecode ? Functions::cbc('package') : 'package') => $this->Package, + ($bytecode ? Functions::cbc('version') : 'version') => $this->Version, + ($bytecode ? Functions::cbc('pointers') : 'pointers') => $pointers + ]; } /** @@ -161,9 +189,18 @@ { $object = new self(); - foreach($data as $datum) + $object->Version = Functions::array_bc($data, 'version'); + $object->Package = Functions::array_bc($data, 'package'); + $object->Pointers = Functions::array_bc($data, 'pointers'); + + if($object->Pointers !== null) { - $object->Pointers[] = ExecutionPointer::fromArray($datum); + $pointers = []; + foreach($object->Pointers as $pointer) + { + $pointers[] = ExecutionPointer::fromArray($pointer); + } + $object->Pointers = $pointers; } return $object; diff --git a/src/ncc/Objects/ExecutionPointers/ExecutionPointer.php b/src/ncc/Objects/ExecutionPointers/ExecutionPointer.php index 9a5747e..e736eca 100644 --- a/src/ncc/Objects/ExecutionPointers/ExecutionPointer.php +++ b/src/ncc/Objects/ExecutionPointers/ExecutionPointer.php @@ -1,4 +1,24 @@ ExecutionPolicy = Functions::array_bc($data, 'execution_policy'); $object->FilePointer = Functions::array_bc($data, 'file_pointer'); + if($object->ExecutionPolicy !== null) + $object->ExecutionPolicy = ExecutionPolicy::fromArray($object->ExecutionPolicy); + return $object; } } \ No newline at end of file diff --git a/src/ncc/Objects/HttpRequest.php b/src/ncc/Objects/HttpRequest.php new file mode 100644 index 0000000..138f76e --- /dev/null +++ b/src/ncc/Objects/HttpRequest.php @@ -0,0 +1,128 @@ +Type = HttpRequestType::GET; + $this->Body = null; + $this->Headers = [ + 'User-Agent: ncc/1.0' + ]; + $this->Options = []; + } + + /** + * Returns an array representation of the object. + * + * @return array + */ + public function toArray(): array + { + return [ + 'type' => $this->Type, + 'url' => $this->Url, + 'headers' => $this->Headers, + 'body' => $this->Body, + 'authentication' => $this->Authentication, + 'options' => $this->Options + ]; + } + + /** + * Returns the hash of the object. + * (This is used for caching) + * + * @return string + */ + public function requestHash(): string + { + return hash('sha1', json_encode($this->toArray())); + } + + /** + * Constructs a new HttpRequest object from an array representation. + * + * @param array $data + * @return static + */ + public static function fromArray(array $data): self + { + $request = new self(); + $request->Type = $data['type']; + $request->Url = $data['url']; + $request->Headers = $data['headers']; + $request->Body = $data['body']; + $request->Authentication = $data['authentication']; + $request->Options = $data['options']; + return $request; + } + } \ No newline at end of file diff --git a/src/ncc/Objects/HttpResponse.php b/src/ncc/Objects/HttpResponse.php new file mode 100644 index 0000000..d412b99 --- /dev/null +++ b/src/ncc/Objects/HttpResponse.php @@ -0,0 +1,70 @@ +StatusCode = 0; + $this->Headers = []; + $this->Body = ''; + } + + /** + * Returns an array representation of the object. + * + * @return array + */ + public function toArray(): array + { + return [ + 'status_code' => $this->StatusCode, + 'headers' => $this->Headers, + 'body' => $this->Body + ]; + } + } \ No newline at end of file diff --git a/src/ncc/Objects/HttpResponseCache.php b/src/ncc/Objects/HttpResponseCache.php new file mode 100644 index 0000000..5ac68a0 --- /dev/null +++ b/src/ncc/Objects/HttpResponseCache.php @@ -0,0 +1,74 @@ +httpResponse = $httpResponse; + $this->ttl = $ttl; + } + + /** + * Returns the cached response + * + * @return HttpResponse + */ + public function getHttpResponse(): HttpResponse + { + return $this->httpResponse; + } + + /** + * Returns the Unix Timestamp of when the cache becomes invalid + * + * @return int + */ + public function getTtl(): int + { + return $this->ttl; + } + } \ No newline at end of file diff --git a/src/ncc/Objects/InstallationPaths.php b/src/ncc/Objects/InstallationPaths.php index d7c745a..b39225e 100644 --- a/src/ncc/Objects/InstallationPaths.php +++ b/src/ncc/Objects/InstallationPaths.php @@ -1,4 +1,24 @@ Resources = []; } + /** + * Adds a dependency to the package + * + * @param Dependency $dependency + * @return void + */ + public function addDependency(Dependency $dependency): void + { + foreach($this->Dependencies as $dep) + { + if($dep->Name == $dependency->Name) + { + $this->removeDependency($dep->Name); + break; + } + } + + $this->Dependencies[] = $dependency; + } + + /** + * Removes a dependency from the build + * + * @param string $name + * @return void + */ + private function removeDependency(string $name): void + { + foreach($this->Dependencies as $key => $dep) + { + if($dep->Name == $name) + { + unset($this->Dependencies[$key]); + return; + } + } + } + /** * Validates the package object and returns True if the package contains the correct information * diff --git a/src/ncc/Objects/Package/Component.php b/src/ncc/Objects/Package/Component.php index b48708b..8a464e0 100644 --- a/src/ncc/Objects/Package/Component.php +++ b/src/ncc/Objects/Package/Component.php @@ -1,4 +1,24 @@ ExecutionPolicy = Functions::array_bc($data, 'execution_policy'); $object->Data = Functions::array_bc($data, 'data'); + if($object->ExecutionPolicy !== null) + $object->ExecutionPolicy = ExecutionPolicy::fromArray($object->ExecutionPolicy); + return $object; } diff --git a/src/ncc/Objects/Package/Header.php b/src/ncc/Objects/Package/Header.php index 34804de..b53779d 100644 --- a/src/ncc/Objects/Package/Header.php +++ b/src/ncc/Objects/Package/Header.php @@ -1,10 +1,31 @@ $this->CompilerExtension->toArray($bytecode), + ($bytecode ? Functions::cbc('compiler_extension') : 'compiler_extension') => $this->CompilerExtension->toArray(), ($bytecode ? Functions::cbc('runtime_constants') : 'runtime_constants') => $this->RuntimeConstants, ($bytecode ? Functions::cbc('compiler_version') : 'compiler_version') => $this->CompilerVersion, + ($bytecode ? Functions::cbc('update_source') : 'update_source') => ($this->UpdateSource?->toArray($bytecode) ?? null), ($bytecode ? Functions::cbc('options') : 'options') => $this->Options, ]; } @@ -76,10 +105,13 @@ $object->CompilerExtension = Functions::array_bc($data, 'compiler_extension'); $object->RuntimeConstants = Functions::array_bc($data, 'runtime_constants'); $object->CompilerVersion = Functions::array_bc($data, 'compiler_version'); + $object->UpdateSource = Functions::array_bc($data, 'update_source'); $object->Options = Functions::array_bc($data, 'options'); if($object->CompilerExtension !== null) $object->CompilerExtension = Compiler::fromArray($object->CompilerExtension); + if($object->UpdateSource !== null) + $object->UpdateSource = UpdateSource::fromArray($object->UpdateSource); return $object; } diff --git a/src/ncc/Objects/Package/Installer.php b/src/ncc/Objects/Package/Installer.php index c368aac..bf949d0 100644 --- a/src/ncc/Objects/Package/Installer.php +++ b/src/ncc/Objects/Package/Installer.php @@ -1,4 +1,24 @@ Assembly->Package} to package lock file"); + if(!isset($this->Packages[$package->Assembly->Package])) { $package_entry = new PackageEntry(); $package_entry->addVersion($package, $install_path, true); $package_entry->Name = $package->Assembly->Package; + $package_entry->UpdateSource = $package->Header->UpdateSource; + $package_entry->getDataPath(); $this->Packages[$package->Assembly->Package] = $package_entry; $this->update(); return; } - $this->Packages[$package->Assembly->Package]->addVersion($package, true); + $this->Packages[$package->Assembly->Package]->UpdateSource = $package->Header->UpdateSource; + $this->Packages[$package->Assembly->Package]->addVersion($package, $install_path, true); + $this->Packages[$package->Assembly->Package]->getDataPath(); $this->update(); } @@ -82,6 +114,8 @@ */ public function removePackageVersion(string $package, string $version): bool { + Console::outVerbose(sprintf('Removing package %s version %s from package lock file', $package, $version)); + if(isset($this->Packages[$package])) { $r = $this->Packages[$package]->removeVersion($version); @@ -104,12 +138,15 @@ * * @param string $package * @return bool + * @noinspection PhpUnused */ public function removePackage(string $package): bool { + Console::outVerbose(sprintf('Removing package %s from package lock file', $package)); if(isset($this->Packages[$package])) { unset($this->Packages[$package]); + $this->update(); return true; } @@ -124,6 +161,8 @@ */ public function getPackage(string $package): ?PackageEntry { + Console::outDebug(sprintf('getting package %s from package lock file', $package)); + if(isset($this->Packages[$package])) { return $this->Packages[$package]; @@ -132,6 +171,40 @@ return null; } + /** + * Determines if the requested package exists in the package lock + * + * @param string $package + * @param string|null $version + * @return bool + */ + public function packageExists(string $package, ?string $version=null): bool + { + $package_entry = $this->getPackage($package); + if($package_entry == null) + return false; + + if($version !== null) + { + try + { + $version_entry = $package_entry->getVersion($version); + } + catch (VersionNotFoundException $e) + { + unset($e); + return false; + } + + if($version_entry == null) + { + return false; + } + } + + return true; + } + /** * Returns an array of all packages and their installed versions * diff --git a/src/ncc/Objects/PackageLock/DependencyEntry.php b/src/ncc/Objects/PackageLock/DependencyEntry.php index 82be14e..bbd1db2 100644 --- a/src/ncc/Objects/PackageLock/DependencyEntry.php +++ b/src/ncc/Objects/PackageLock/DependencyEntry.php @@ -1,4 +1,24 @@ LatestVersion; + foreach($this->Versions as $versionEntry) { if($versionEntry->Version == $version) @@ -128,6 +166,9 @@ $version->MainExecutionPolicy = $package->MainExecutionPolicy; $version->Location = $install_path; + foreach($version->ExecutionUnits as $unit) + $unit->Data = null; + foreach($package->Dependencies as $dependency) { $version->Dependencies[] = new DependencyEntry($dependency); @@ -186,6 +227,24 @@ return $r; } + /** + * @return string + * @throws InvalidPackageNameException + * @throws InvalidScopeException + */ + public function getDataPath(): string + { + $path = PathFinder::getPackageDataPath($this->Name); + + if(!file_exists($path) && Resolver::resolveScope() == Scopes::System) + { + $filesystem = new Filesystem(); + $filesystem->mkdir($path); + } + + return $path; + } + /** * Returns an array representation of the object * @@ -204,6 +263,7 @@ ($bytecode ? Functions::cbc('name') : 'name') => $this->Name, ($bytecode ? Functions::cbc('latest_version') : 'latest_version') => $this->LatestVersion, ($bytecode ? Functions::cbc('versions') : 'versions') => $versions, + ($bytecode ? Functions::cbc('update_source') : 'update_source') => ($this->UpdateSource?->toArray($bytecode) ?? null), ]; } @@ -220,6 +280,10 @@ $object->Name = Functions::array_bc($data, 'name'); $object->LatestVersion = Functions::array_bc($data, 'latest_version'); $versions = Functions::array_bc($data, 'versions'); + $object->UpdateSource = Functions::array_bc($data, 'update_source'); + + if($object->UpdateSource !== null) + $object->UpdateSource = UpdateSource::fromArray($object->UpdateSource); if($versions !== null) { diff --git a/src/ncc/Objects/PackageLock/VersionEntry.php b/src/ncc/Objects/PackageLock/VersionEntry.php index c4fe59f..a90a22b 100644 --- a/src/ncc/Objects/PackageLock/VersionEntry.php +++ b/src/ncc/Objects/PackageLock/VersionEntry.php @@ -1,4 +1,24 @@ $this->Version, - ($bytecode ? Functions::cbc('compiler') : 'compiler') => $this->Compiler->toArray($bytecode), + ($bytecode ? Functions::cbc('compiler') : 'compiler') => $this->Compiler->toArray(), ($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, diff --git a/src/ncc/Objects/PhpConfiguration.php b/src/ncc/Objects/PhpConfiguration.php index e6d4e50..329ae8e 100644 --- a/src/ncc/Objects/PhpConfiguration.php +++ b/src/ncc/Objects/PhpConfiguration.php @@ -1,4 +1,24 @@ Build->Main !== null) + { + if($this->ExecutionPolicies == null || count($this->ExecutionPolicies) == 0) + { + if($throw_exception) + throw new UndefinedExecutionPolicyException(sprintf('Build configuration build.main uses an execution policy "%s" but no policies are defined', $this->Build->Main)); + return false; + } + + + $found = false; + foreach($this->ExecutionPolicies as $policy) + { + if($policy->Name == $this->Build->Main) + { + $found = true; + break; + } + } + + if(!$found) + { + if($throw_exception) + throw new UndefinedExecutionPolicyException(sprintf('Build configuration build.main points to a undefined execution policy "%s"', $this->Build->Main)); + return false; + } + + if($this->Build->Main == BuildConfigurationValues::AllConfigurations) + { + if($throw_exception) + throw new InvalidBuildConfigurationException(sprintf('Build configuration build.main cannot be set to "%s"', BuildConfigurationValues::AllConfigurations)); + return false; + } + } + return true; } diff --git a/src/ncc/Objects/ProjectConfiguration/Assembly.php b/src/ncc/Objects/ProjectConfiguration/Assembly.php index 46e4af4..ff2259f 100644 --- a/src/ncc/Objects/ProjectConfiguration/Assembly.php +++ b/src/ncc/Objects/ProjectConfiguration/Assembly.php @@ -1,4 +1,24 @@ Configurations = []; } + /** + * Adds a new dependency to the build, if it doesn't already exist + * + * @param Dependency $dependency + * @return void + */ + public function addDependency(Dependency $dependency): void + { + foreach($this->Dependencies as $dep) + { + if($dep->Name == $dependency->Name) + { + $this->removeDependency($dep->Name); + break; + } + } + + $this->Dependencies[] = $dependency; + } + + /** + * Removes a dependency from the build + * + * @param string $name + * @return void + */ + private function removeDependency(string $name): void + { + foreach($this->Dependencies as $key => $dep) + { + if($dep->Name == $name) + { + unset($this->Dependencies[$key]); + return; + } + } + } + /** * Validates the build configuration object * * @param bool $throw_exception * @return bool + * @throws BuildConfigurationNotFoundException + * @throws InvalidBuildConfigurationException * @throws InvalidConstantNameException * @throws InvalidProjectBuildConfiguration */ public function validate(bool $throw_exception=True): bool { - // TODO: Implement validation for Configurations, Dependencies and ExcludedFiles - // Check the defined constants foreach($this->DefineConstants as $name => $value) { @@ -140,6 +200,37 @@ } } + foreach($this->Configurations as $configuration) + { + try + { + if (!$configuration->validate($throw_exception)) + return false; + } + catch (InvalidBuildConfigurationException $e) + { + throw new InvalidBuildConfigurationException(sprintf('Error in build configuration \'%s\'', $configuration->Name), $e); + } + } + + if($this->DefaultConfiguration == null) + { + if($throw_exception) + throw new InvalidProjectBuildConfiguration('The default build configuration is not set'); + + return false; + } + + if(!Validate::nameFriendly($this->DefaultConfiguration)) + { + if($throw_exception) + throw new InvalidProjectBuildConfiguration('The default build configuration name \'' . $this->DefaultConfiguration . '\' is not valid'); + + return false; + } + + $this->getBuildConfiguration($this->DefaultConfiguration); + return true; } @@ -206,7 +297,7 @@ if($this->Scope !== null) $ReturnResults[($bytecode ? Functions::cbc('scope') : 'scope')] = $this->Scope; if($this->Main !== null) - $ReturnResults[($bytecode ? Functions::cbc('main') : 'main')] = $this->Main; + $ReturnResults[($bytecode ? Functions::cbc('main') : 'main')] = $this->Main; if($this->DefineConstants !== null && count($this->DefineConstants) > 0) $ReturnResults[($bytecode ? Functions::cbc('define_constants') : 'define_constants')] = $this->DefineConstants; if($this->PreBuild !== null && count($this->PreBuild) > 0) diff --git a/src/ncc/Objects/ProjectConfiguration/BuildConfiguration.php b/src/ncc/Objects/ProjectConfiguration/Build/BuildConfiguration.php similarity index 53% rename from src/ncc/Objects/ProjectConfiguration/BuildConfiguration.php rename to src/ncc/Objects/ProjectConfiguration/Build/BuildConfiguration.php index 8991c63..22ed725 100644 --- a/src/ncc/Objects/ProjectConfiguration/BuildConfiguration.php +++ b/src/ncc/Objects/ProjectConfiguration/Build/BuildConfiguration.php @@ -1,10 +1,34 @@ Dependencies = []; } - // TODO: Add a function to validate the object data + /** + * Validates the BuildConfiguration object + * + * @param bool $throw_exception + * @return bool + * @throws InvalidBuildConfigurationException + */ + public function validate(bool $throw_exception=True): bool + { + if(!Validate::nameFriendly($this->Name)) + { + if($throw_exception) + throw new InvalidBuildConfigurationException(sprintf('Invalid build configuration name "%s"', $this->Name)); + + return False; + } + + if(!Validate::pathName($this->OutputPath)) + { + if($throw_exception) + throw new InvalidBuildConfigurationException(sprintf('\'output_path\' contains an invalid path name in %s', $this->Name)); + + return False; + } + + if($this->DefineConstants !== null && !is_array($this->DefineConstants)) + { + if($throw_exception) + throw new InvalidBuildConfigurationException(sprintf('\'define_constants\' must be an array in %s', $this->Name)); + + return False; + } + + if($this->ExcludeFiles !== null && !is_array($this->ExcludeFiles)) + { + if($throw_exception) + throw new InvalidBuildConfigurationException(sprintf('\'exclude_files\' must be an array in %s', $this->Name)); + + return False; + } + + if($this->PreBuild !== null && !is_array($this->PreBuild)) + { + if($throw_exception) + throw new InvalidBuildConfigurationException(sprintf('\'pre_build\' must be an array in %s', $this->Name)); + + return False; + } + + if($this->PostBuild !== null && !is_array($this->PostBuild)) + { + if($throw_exception) + throw new InvalidBuildConfigurationException(sprintf('\'post_build\' must be an array in %s', $this->Name)); + + return False; + } + + if($this->Dependencies !== null && !is_array($this->Dependencies)) + { + if($throw_exception) + throw new InvalidBuildConfigurationException(sprintf('\'dependencies\' must be an array in %s', $this->Name)); + + return False; + } + + /** @var Dependency $dependency */ + foreach($this->Dependencies as $dependency) + { + try + { + if (!$dependency->validate($throw_exception)) + return False; + } + catch (InvalidDependencyConfiguration $e) + { + if($throw_exception) + throw new InvalidBuildConfigurationException(sprintf('Invalid dependency configuration in %s: %s', $this->Name, $e->getMessage())); + + return False; + } + } + + return True; + } /** * Returns an array representation of the object diff --git a/src/ncc/Objects/ProjectConfiguration/Compiler.php b/src/ncc/Objects/ProjectConfiguration/Compiler.php index 9796150..64ce759 100644 --- a/src/ncc/Objects/ProjectConfiguration/Compiler.php +++ b/src/ncc/Objects/ProjectConfiguration/Compiler.php @@ -1,4 +1,24 @@ Extension !== null && strlen($this->Extension) > 0) diff --git a/src/ncc/Objects/ProjectConfiguration/Dependency.php b/src/ncc/Objects/ProjectConfiguration/Dependency.php index 3fa3116..46b6f8c 100644 --- a/src/ncc/Objects/ProjectConfiguration/Dependency.php +++ b/src/ncc/Objects/ProjectConfiguration/Dependency.php @@ -1,11 +1,32 @@ Name)) + { + if($throw_exception) + throw new InvalidDependencyConfiguration(sprintf('Invalid dependency name "%s"', $this->Name)); + + return false; + } + + if($this->Version !== null && !Validate::version($this->Version)) + { + if($throw_exception) + throw new InvalidDependencyConfiguration(sprintf('Invalid dependency version "%s"', $this->Version)); + + return false; + } + + return true; + } } \ No newline at end of file diff --git a/src/ncc/Objects/ProjectConfiguration/ExecutionPolicy.php b/src/ncc/Objects/ProjectConfiguration/ExecutionPolicy.php index e0b0a58..efc175c 100644 --- a/src/ncc/Objects/ProjectConfiguration/ExecutionPolicy.php +++ b/src/ncc/Objects/ProjectConfiguration/ExecutionPolicy.php @@ -1,10 +1,29 @@ 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->Execute = Functions::array_bc($data, 'execute'); $object->ExitHandlers = Functions::array_bc($data, 'exit_handlers'); if($object->Execute !== null) diff --git a/src/ncc/Objects/ProjectConfiguration/ExecutionPolicy/Execute.php b/src/ncc/Objects/ProjectConfiguration/ExecutionPolicy/Execute.php index 8fe5b02..6839d88 100644 --- a/src/ncc/Objects/ProjectConfiguration/ExecutionPolicy/Execute.php +++ b/src/ncc/Objects/ProjectConfiguration/ExecutionPolicy/Execute.php @@ -1,4 +1,24 @@ Tty = false; $this->Silent = false; $this->Timeout = null; + $this->IdleTimeout = null; $this->WorkingDirectory = "%CWD%"; } @@ -82,6 +115,9 @@ if($this->Options !== null) $results[($bytecode ? Functions::cbc("options") : "options")] = $this->Options; + if($this->EnvironmentVariables !== null) + $results[($bytecode ? Functions::cbc("environment_variables") : "environment_variables")] = $this->EnvironmentVariables; + if($this->Silent !== null) $results[($bytecode ? Functions::cbc("silent") : "silent")] = (bool)$this->Silent; @@ -91,6 +127,9 @@ if($this->Timeout !== null) $results[($bytecode ? Functions::cbc("timeout") : "timeout")] = (int)$this->Timeout; + if($this->IdleTimeout !== null) + $results[($bytecode ? Functions::cbc("idle_timeout") : "idle_timeout")] = (int)$this->IdleTimeout; + return $results; } @@ -107,9 +146,11 @@ $object->Target = Functions::array_bc($data, 'target'); $object->WorkingDirectory = Functions::array_bc($data, 'working_directory'); $object->Options = Functions::array_bc($data, 'options'); + $object->EnvironmentVariables = Functions::array_bc($data, 'environment_variables'); $object->Silent = Functions::array_bc($data, 'silent'); $object->Tty = Functions::array_bc($data, 'tty'); $object->Timeout = Functions::array_bc($data, 'timeout'); + $object->IdleTimeout = Functions::array_bc($data, 'idle_timeout'); return $object; } diff --git a/src/ncc/Objects/ProjectConfiguration/ExecutionPolicy/ExitHandle.php b/src/ncc/Objects/ProjectConfiguration/ExecutionPolicy/ExitHandle.php index 98ea264..176858f 100644 --- a/src/ncc/Objects/ProjectConfiguration/ExecutionPolicy/ExitHandle.php +++ b/src/ncc/Objects/ProjectConfiguration/ExecutionPolicy/ExitHandle.php @@ -1,4 +1,24 @@ Compiler->toArray($bytecode); + $ReturnResults[($bytecode ? Functions::cbc('compiler') : 'compiler')] = $this->Compiler->toArray(); $ReturnResults[($bytecode ? Functions::cbc('options') : 'options')] = $this->Options; + if($this->UpdateSource !== null) + $ReturnResults[($bytecode ? Functions::cbc('update_source') : 'update_source')] = $this->UpdateSource->toArray($bytecode); + return $ReturnResults; } @@ -89,6 +117,11 @@ $ProjectObject->Options = Functions::array_bc($data, 'options'); } + if(Functions::array_bc($data, 'update_source') !== null) + { + $ProjectObject->UpdateSource = UpdateSource::fromArray(Functions::array_bc($data, 'update_source')); + } + return $ProjectObject; } } \ No newline at end of file diff --git a/src/ncc/Objects/ProjectConfiguration/UpdateSource.php b/src/ncc/Objects/ProjectConfiguration/UpdateSource.php index b9344e7..1cb0c97 100644 --- a/src/ncc/Objects/ProjectConfiguration/UpdateSource.php +++ b/src/ncc/Objects/ProjectConfiguration/UpdateSource.php @@ -1,8 +1,78 @@ $this->Source, + ($bytecode ? Functions::cbc('repository') : 'repository') => ($this->Repository?->toArray($bytecode)) + ]; + } + + + /** + * Constructs object from an array representation + * + * @param array $data + * @return UpdateSource + */ + public static function fromArray(array $data): UpdateSource + { + $obj = new UpdateSource(); + $obj->Source = Functions::array_bc($data, 'source'); + $obj->Repository = Functions::array_bc($data, 'repository'); + + if($obj->Repository !== null) + $obj->Repository = Repository::fromArray($obj->Repository); + + return $obj; + } } \ No newline at end of file diff --git a/src/ncc/Objects/ProjectConfiguration/UpdateSource/Repository.php b/src/ncc/Objects/ProjectConfiguration/UpdateSource/Repository.php new file mode 100644 index 0000000..395e215 --- /dev/null +++ b/src/ncc/Objects/ProjectConfiguration/UpdateSource/Repository.php @@ -0,0 +1,90 @@ + $this->Name, + ($bytecode ? Functions::cbc('type') : 'type') => $this->Type, + ($bytecode ? Functions::cbc('host') : 'host') => $this->Host, + ($bytecode ? Functions::cbc('ssl') : 'ssl') => $this->SSL + ]; + } + + /** + * Constructs object from an array representation + * + * @param array $data + * @return Repository + */ + public static function fromArray(array $data): self + { + $obj = new self(); + $obj->Name = Functions::array_bc($data, 'name'); + $obj->Type = Functions::array_bc($data, 'type'); + $obj->Host = Functions::array_bc($data, 'host'); + $obj->SSL = Functions::array_bc($data, 'ssl'); + return $obj; + } + } \ No newline at end of file diff --git a/src/ncc/Objects/ProjectDetectionResults.php b/src/ncc/Objects/ProjectDetectionResults.php new file mode 100644 index 0000000..b5bb92e --- /dev/null +++ b/src/ncc/Objects/ProjectDetectionResults.php @@ -0,0 +1,78 @@ +Files = new Files(); + } + } \ No newline at end of file diff --git a/src/ncc/Objects/RepositoryQueryResults/Files.php b/src/ncc/Objects/RepositoryQueryResults/Files.php new file mode 100644 index 0000000..bc11788 --- /dev/null +++ b/src/ncc/Objects/RepositoryQueryResults/Files.php @@ -0,0 +1,70 @@ +ExecutionPolicyName = 'main'; + $this->Registered = false; + } + + /** + * Returns a string representation of the object + * + * @param bool $bytecode + * @return array + */ + public function toArray(bool $bytecode=false): array + { + return [ + ($bytecode ? Functions::cbc('package') : 'package') => $this->Package, + ($bytecode ? Functions::cbc('registered') : 'registered') => $this->Registered, + ($bytecode ? Functions::cbc('execution_policy_name') : 'execution_policy_name') => $this->ExecutionPolicyName + ]; + } + + /** + * Constructs a new SymlinkEntry from an array representation + * + * @param array $data + * @return SymlinkEntry + */ + public static function fromArray(array $data): SymlinkEntry + { + $entry = new SymlinkEntry(); + + $entry->Package = Functions::array_bc($data, 'package'); + $entry->Registered = (bool)Functions::array_bc($data, 'registered'); + $entry->ExecutionPolicyName = Functions::array_bc($data, 'execution_policy_name'); + + return $entry; + } + + } \ No newline at end of file diff --git a/src/ncc/Objects/Vault.php b/src/ncc/Objects/Vault.php index 8a60105..f3e9517 100644 --- a/src/ncc/Objects/Vault.php +++ b/src/ncc/Objects/Vault.php @@ -1,9 +1,35 @@ Version = Versions::CredentialsStoreVersion; $this->Entries = []; } + /** + * Adds a new entry to the vault + * + * @param string $name + * @param PasswordInterface $password + * @param bool $encrypt + * @return bool + * @noinspection PhpUnused + */ + public function addEntry(string $name, PasswordInterface $password, bool $encrypt=true): bool + { + // Check if the entry already exists + foreach($this->Entries as $entry) + { + if($entry->getName() === $name) + return false; + } + + // Create the new entry + $entry = new Entry(); + $entry->setName($name); + $entry->setEncrypted($encrypt); + $entry->setAuthentication($password); + + // Add the entry to the vault + $this->Entries[] = $entry; + return true; + } + + /** + * Deletes an entry from the vault + * + * @param string $name + * @return bool + * @noinspection PhpUnused + */ + public function deleteEntry(string $name): bool + { + // Find the entry + foreach($this->Entries as $index => $entry) + { + if($entry->getName() === $name) + { + // Remove the entry + unset($this->Entries[$index]); + return true; + } + } + + // Entry not found + return false; + } + + /** + * Returns all the entries in the vault + * + * @return array|Entry[] + * @noinspection PhpUnused + */ + public function getEntries(): array + { + return $this->Entries; + } + + /** + * Returns an existing entry from the vault + * + * @param string $name + * @return Entry|null + */ + public function getEntry(string $name): ?Entry + { + foreach($this->Entries as $entry) + { + if($entry->getName() === $name) + return $entry; + } + + return null; + } + + /** + * Authenticates an entry in the vault + * + * @param string $name + * @param string $password + * @return bool + * @throws RuntimeException + * @noinspection PhpUnused + */ + public function authenticate(string $name, string $password): bool + { + $entry = $this->getEntry($name); + if($entry === null) + return false; + + if($entry->getPassword() === null) + { + if($entry->isEncrypted() && !$entry->isCurrentlyDecrypted()) + { + return $entry->unlock($password); + } + } + + $input = []; + switch($entry->getPassword()->getAuthenticationType()) + { + case AuthenticationType::UsernamePassword: + $input = ['password' => $password]; + break; + case AuthenticationType::AccessToken: + $input = ['token' => $password]; + break; + } + + return $entry->authenticate($input); + } + /** * Returns an array representation of the object * + * @param bool $bytecode * @return array */ - public function toArray(): array + public function toArray(bool $bytecode=false): array { - $Entries = []; - + $entries = []; foreach($this->Entries as $entry) { - $Entries[] = $entry->toArray(); + $entries[] = $entry->toArray($bytecode); } return [ - 'version' => $this->Version, - 'entries' => $Entries + ($bytecode ? Functions::cbc('version') : 'version') => $this->Version, + ($bytecode ? Functions::cbc('entries') : 'entries') => $entries, ]; } /** - * Constructs an object from an array representation + * Constructs a new object from an array * - * @param array $data + * @param array $array * @return Vault */ - public static function fromArray(array $data): Vault + public static function fromArray(array $array): Vault { - $VaultObject = new Vault(); + $vault = new Vault(); + $vault->Version = Functions::array_bc($array, 'version'); + $entries = Functions::array_bc($array, 'entries'); + $vault->Entries = []; - if(isset($data['version'])) - $VaultObject->Version = $data['version']; + foreach($entries as $entry) + $vault->Entries[] = Entry::fromArray($entry); - if(isset($data['entries'])) - { - foreach($data['entries'] as $entry) - { - $VaultObject->Entries[] = Entry::fromArray($entry); - } - } - - return $VaultObject; + return $vault; } + } \ No newline at end of file diff --git a/src/ncc/Objects/Vault/DefaultEntry.php b/src/ncc/Objects/Vault/DefaultEntry.php deleted file mode 100644 index c834f0d..0000000 --- a/src/ncc/Objects/Vault/DefaultEntry.php +++ /dev/null @@ -1,60 +0,0 @@ - $this->Alias, - 'source' => $this->Source - ]; - } - - /** - * Constructs the object from an array representation - * - * @param array $data - * @return DefaultEntry - */ - public static function fromArray(array $data): DefaultEntry - { - $DefaultEntryObject = new DefaultEntry(); - - if(isset($data['alias'])) - { - $DefaultEntryObject->Alias = $data['alias']; - } - - if(isset($data['source'])) - { - $DefaultEntryObject->Source = $data['source']; - } - - return $DefaultEntryObject; - } - - } \ No newline at end of file diff --git a/src/ncc/Objects/Vault/Entry.php b/src/ncc/Objects/Vault/Entry.php index 27c778f..4445ca7 100644 --- a/src/ncc/Objects/Vault/Entry.php +++ b/src/ncc/Objects/Vault/Entry.php @@ -1,127 +1,355 @@ Encrypted = true; + $this->IsCurrentlyDecrypted = true; + } + + /** + * Test Authenticates the entry + * + * For UsernamePassword the $input parameter expects an array with the keys 'username' and 'password' + * For AccessToken the $input parameter expects an array with the key 'token' + * + * @param array $input + * @return bool + * @noinspection PhpUnused + */ + public function authenticate(array $input): bool + { + if(!$this->IsCurrentlyDecrypted) + return false; + + if($this->Password == null) + return false; + + switch($this->Password->getAuthenticationType()) + { + case AuthenticationType::UsernamePassword: + if(!($this->Password instanceof UsernamePassword)) + return false; + + $username = $input['username'] ?? null; + $password = $input['password'] ?? null; + + if($username === null && $password === null) + return false; + + if($username == null) + return $password == $this->Password->getPassword(); + + if($password == null) + return $username == $this->Password->getUsername(); + + return $username == $this->Password->getUsername() && $password == $this->Password->getPassword(); + + case AuthenticationType::AccessToken: + if(!($this->Password instanceof AccessToken)) + return false; + + $token = $input['token'] ?? null; + + if($token === null) + return false; + + return $token == $this->Password->AccessToken; + + default: + return false; + + } + } + + /** + * @param PasswordInterface $password + * @return void + */ + public function setAuthentication(PasswordInterface $password): void + { + $this->Password = $password; + } + + /** + * @return bool + * @noinspection PhpUnused + */ + public function isCurrentlyDecrypted(): bool + { + return $this->IsCurrentlyDecrypted; + } + + /** + * Locks the entry by encrypting the password + * + * @return bool + */ + public function lock(): bool + { + if($this->Password == null) + return false; + + if($this->Encrypted) + return false; + + if(!$this->IsCurrentlyDecrypted) + return false; + + if(!($this->Password instanceof PasswordInterface)) + return false; + + $this->Password = $this->encrypt(); + return true; + } + + /** + * Unlocks the entry by decrypting the password + * + * @param string $password + * @return bool + * @throws RuntimeException + * @noinspection PhpUnused + */ + public function unlock(string $password): bool + { + if($this->Password == null) + return false; + + if(!$this->Encrypted) + return false; + + if($this->IsCurrentlyDecrypted) + return false; + + if(!is_string($this->Password)) + return false; + + try + { + $password = Crypto::decryptWithPassword($this->Password, $password, true); + } + catch (EnvironmentIsBrokenException $e) + { + throw new RuntimeException('Cannot decrypt password', $e); + } + catch (WrongKeyOrModifiedCiphertextException $e) + { + unset($e); + return false; + } + + $this->Password = ZiProto::decode($password); + $this->IsCurrentlyDecrypted = true; + + return true; + } + + /** + * Returns the password object as an encrypted binary string + * + * @return string|null + */ + private function encrypt(): ?string + { + if(!$this->IsCurrentlyDecrypted) + return false; + + if($this->Password == null) + return false; + + if(!($this->Password instanceof PasswordInterface)) + return null; + + $data = ZiProto::encode($this->Password->toArray(true)); + return Crypto::encryptWithPassword($data, (string)$this->Password, true); + } + + /** + * Returns an array representation of the object + * + * @param bool $bytecode + * @return array + */ + public function toArray(bool $bytecode=false): array + { + if($this->Password !== null) + { + if($this->Encrypted && $this->IsCurrentlyDecrypted) + { + $password = $this->encrypt(); + } + elseif($this->Encrypted) + { + $password = $this->Password; + } + else + { + $password = $this->Password->toArray(true); + } + } + else + { + $password = $this->Password; + } + return [ - 'alias' => $this->Alias, - 'source' => $this->Source, - 'source_host' => $this->SourceHost, - 'authentication_type' => $this->AuthenticationType, - 'encrypted' => $this->Encrypted, - 'authentication' => $this->Authentication + ($bytecode ? Functions::cbc('name') : 'name') => $this->Name, + ($bytecode ? Functions::cbc('encrypted') : 'encrypted') => $this->Encrypted, + ($bytecode ? Functions::cbc('password') : 'password') => $password, ]; } /** - * Returns an array representation of the object + * Constructs an object from an array representation * * @param array $data * @return Entry */ - public static function fromArray(array $data): Entry + public static function fromArray(array $data): self { - $EntryObject = new Entry(); + $self = new self(); - if(isset($data['alias'])) + $self->Name = Functions::array_bc($data, 'name'); + $self->Encrypted = Functions::array_bc($data, 'encrypted'); + $password = Functions::array_bc($data, 'password'); + + if($password !== null) { - $EntryObject->Alias = $data['alias']; + if($self->Encrypted) + { + $self->Password = $password; + $self->IsCurrentlyDecrypted = false; + } + elseif(gettype($password) == 'array') + { + $self->Password = match (Functions::array_bc($password, 'authentication_type')) + { + AuthenticationType::UsernamePassword => UsernamePassword::fromArray($password), + AuthenticationType::AccessToken => AccessToken::fromArray($password) + }; + } } - if(isset($data['source'])) - { - $EntryObject->Source = $data['source']; - } + return $self; + } - if(isset($data['source_host'])) - { - $EntryObject->SourceHost = $data['source_host']; - } + /** + * @return bool + */ + public function isEncrypted(): bool + { + return $this->Encrypted; + } - if(isset($data['authentication_type'])) - { - $EntryObject->AuthenticationType = $data['authentication_type']; - } + /** + * Returns false if the entry needs to be decrypted first + * + * @param bool $Encrypted + * @return bool + */ + public function setEncrypted(bool $Encrypted): bool + { + if(!$this->IsCurrentlyDecrypted) + return false; - if(isset($data['encrypted'])) - { - $EntryObject->Encrypted = $data['encrypted']; - } + $this->Encrypted = $Encrypted; + return true; + } - if(isset($data['authentication'])) - { - $EntryObject->Authentication = $data['authentication']; - } + /** + * @return string + */ + public function getName(): string + { + return $this->Name; + } - return $EntryObject; + /** + * @param string $Name + */ + public function setName(string $Name): void + { + $this->Name = $Name; + } + + /** + * @return PasswordInterface|null + */ + public function getPassword(): ?PasswordInterface + { + if(!$this->IsCurrentlyDecrypted) + return null; + + return $this->Password; } } \ No newline at end of file diff --git a/src/ncc/Objects/Vault/Password/AccessToken.php b/src/ncc/Objects/Vault/Password/AccessToken.php new file mode 100644 index 0000000..66b2d69 --- /dev/null +++ b/src/ncc/Objects/Vault/Password/AccessToken.php @@ -0,0 +1,102 @@ + AuthenticationType::AccessToken, + ($bytecode ? Functions::cbc('access_token') : 'access_token') => $this->AccessToken, + ]; + } + + /** + * Constructs an object from an array representation + * + * @param array $data + * @return static + */ + public static function fromArray(array $data): self + { + $object = new self(); + + $object->AccessToken = Functions::array_bc($data, 'access_token'); + + return $object; + } + + /** + * @return string + */ + public function getAccessToken(): string + { + return $this->AccessToken; + } + + /** + * @inheritDoc + */ + public function getAuthenticationType(): string + { + return AuthenticationType::AccessToken; + } + + /** + * Returns a string representation of the object + * + * @return string + */ + public function __toString(): string + { + return $this->AccessToken; + } + + /** + * @param string $AccessToken + */ + public function setAccessToken(string $AccessToken): void + { + $this->AccessToken = $AccessToken; + } + } \ No newline at end of file diff --git a/src/ncc/Objects/Vault/Password/UsernamePassword.php b/src/ncc/Objects/Vault/Password/UsernamePassword.php new file mode 100644 index 0000000..de11f39 --- /dev/null +++ b/src/ncc/Objects/Vault/Password/UsernamePassword.php @@ -0,0 +1,129 @@ + AuthenticationType::UsernamePassword, + ($bytecode ? Functions::cbc('username') : 'username') => $this->Username, + ($bytecode ? Functions::cbc('password') : 'password') => $this->Password, + ]; + } + + /** + * Constructs an object from an array representation + * + * @param array $data + * @return static + */ + public static function fromArray(array $data): self + { + $instance = new self(); + + $instance->Username = Functions::array_bc($data, 'username'); + $instance->Password = Functions::array_bc($data, 'password'); + + return $instance; + } + + /** + * @return string + * @noinspection PhpUnused + */ + public function getUsername(): string + { + return $this->Username; + } + + /** + * @return string + * @noinspection PhpUnused + */ + public function getPassword(): string + { + return $this->Password; + } + + /** + * @inheritDoc + */ + public function getAuthenticationType(): string + { + return AuthenticationType::UsernamePassword; + } + + /** + * Returns a string representation of the object + * + * @return string + */ + public function __toString(): string + { + return $this->Password; + } + + /** + * @param string $Username + */ + public function setUsername(string $Username): void + { + $this->Username = $Username; + } + + /** + * @param string $Password + */ + public function setPassword(string $Password): void + { + $this->Password = $Password; + } + } \ No newline at end of file diff --git a/src/ncc/Runtime.php b/src/ncc/Runtime.php new file mode 100644 index 0000000..b9ab941 --- /dev/null +++ b/src/ncc/Runtime.php @@ -0,0 +1,228 @@ +getPackage($package)->getLatestVersion(); + + $entry = "$package=$version"; + + return isset(self::$imported_packages[$entry]); + } + + /** + * Adds a package to the imported packages list + * + * @param string $package + * @param string $version + * @return void + */ + private static function addImport(string $package, string $version): void + { + $entry = "$package=$version"; + self::$imported_packages[$entry] = true; + } + + + /** + * @param string $package + * @param string $version + * @param array $options + * @return void + * @throws ImportException + */ + public static function import(string $package, string $version=Versions::Latest, array $options=[]): void + { + try + { + $package_entry = self::getPackageManager()->getPackage($package); + } + catch (PackageLockException $e) + { + throw new ImportException(sprintf('Failed to import package "%s" due to a package lock exception: %s', $package, $e->getMessage()), $e); + } + if($package_entry == null) + { + throw new ImportException(sprintf("Package '%s' not found", $package)); + } + + if($version == Versions::Latest) + $version = $package_entry->getLatestVersion(); + + try + { + /** @var VersionEntry $version_entry */ + $version_entry = $package_entry->getVersion($version); + + if($version_entry == null) + throw new VersionNotFoundException(); + } + catch (VersionNotFoundException $e) + { + throw new ImportException(sprintf('Version %s of %s is not installed', $version, $package), $e); + } + + try + { + if (self::isImported($package, $version)) + return; + } + catch (PackageLockException $e) + { + throw new ImportException(sprintf('Failed to check if package %s is imported', $package), $e); + } + + if($version_entry->Dependencies !== null && count($version_entry->Dependencies) > 0) + { + // Import all dependencies first + /** @var Dependency $dependency */ + foreach($version_entry->Dependencies as $dependency) + self::import($dependency->PackageName, $dependency->Version, $options); + } + + try + { + switch($version_entry->Compiler->Extension) + { + case CompilerExtensions::PHP: + PhpRuntime::import($version_entry, $options); + break; + + default: + throw new ImportException(sprintf('Compiler extension %s is not supported in this runtime', $version_entry->Compiler->Extension)); + } + } + catch(Exception $e) + { + throw new ImportException(sprintf('Failed to import package %s', $package), $e); + } + + self::addImport($package, $version); + } + + /** + * Returns the data path of the package + * + * @param string $package + * @return string + * @throws Exceptions\InvalidPackageNameException + * @throws Exceptions\InvalidScopeException + * @throws PackageLockException + * @throws PackageNotFoundException + */ + public static function getDataPath(string $package): string + { + $package = self::getPackageManager()->getPackage($package); + + if($package == null) + throw new PackageNotFoundException('Package not found (null entry error, possible bug)'); + + return $package->getDataPath(); + } + + /** + * @return PackageManager + */ + private static function getPackageManager(): PackageManager + { + if(self::$package_manager == null) + self::$package_manager = new PackageManager(); + return self::$package_manager; + } + + /** + * Returns an array of all the packages that is currently imported + * + * @return array + */ + public static function getImportedPackages(): array + { + return array_keys(self::$imported_packages); + } + + /** + * Returns a registered constant + * + * @param string $package + * @param string $name + * @return string|null + */ + public static function getConstant(string $package, string $name): ?string + { + return Constants::get($package, $name); + } + + /** + * Registers a new constant + * + * @param string $package + * @param string $name + * @param string $value + * @return void + * @throws ConstantReadonlyException + * @throws InvalidConstantNameException + */ + public static function setConstant(string $package, string $name, string $value): void + { + Constants::register($package, $name, $value); + } + } \ No newline at end of file diff --git a/src/ncc/Runtime/Constants.php b/src/ncc/Runtime/Constants.php index 312b443..2b8195a 100644 --- a/src/ncc/Runtime/Constants.php +++ b/src/ncc/Runtime/Constants.php @@ -1,4 +1,24 @@ getValue(); + + return null; + } } \ No newline at end of file diff --git a/src/ncc/Utilities/Base64.php b/src/ncc/Utilities/Base64.php index 9c75af3..d827ea5 100644 --- a/src/ncc/Utilities/Base64.php +++ b/src/ncc/Utilities/Base64.php @@ -1,6 +1,26 @@ 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; + $fmt_tick = $tick_time; + if(self::$lastTickTime !== null) + { + $timeDiff = microtime(true) - self::$lastTickTime; + + if ($timeDiff > 1.0) + { + $fmt_tick = Console::formatColor($tick_time, ConsoleColors::LightRed); + } + elseif ($timeDiff > 0.5) + { + $fmt_tick = Console::formatColor($tick_time, ConsoleColors::LightYellow); + } + } + + self::$lastTickTime = $tick_time; + return '[' . $fmt_tick . '] ' . $input; } /** @@ -179,6 +229,14 @@ $trace_msg .= ' > '; } + /** Apply syntax highlighting using regular expressions */ + + // Hyperlinks + $message = preg_replace('/(https?:\/\/[^\s]+)/', Console::formatColor('$1', ConsoleColors::LightBlue), $message); + + // File Paths + $message = preg_replace('/(\/[^\s]+)/', Console::formatColor('$1', ConsoleColors::LightCyan), $message); + /** @noinspection PhpUnnecessaryStringCastInspection */ $message = self::setPrefix(LogLevel::Debug, (string)$trace_msg . $message); @@ -313,16 +371,20 @@ * Prints out a detailed exception display (unfinished) * * @param Exception $e + * @param bool $sub * @return void */ - private static function outExceptionDetails(Exception $e): void + private static function outExceptionDetails(Throwable $e, bool $sub=false): void { if(!ncc::cliMode()) return; + // Exception name without namespace + $trace_header = self::formatColor($e->getFile() . ':' . $e->getLine(), ConsoleColors::Magenta); - $trace_error = self::formatColor('error: ', ConsoleColors::Red); + $trace_error = self::formatColor( 'Error: ', ConsoleColors::Red); self::out($trace_header . ' ' . $trace_error . $e->getMessage()); + self::out(sprintf('Exception: %s', get_class($e))); self::out(sprintf('Error code: %s', $e->getCode())); $trace = $e->getTrace(); if(count($trace) > 1) @@ -334,7 +396,16 @@ } } - if(Main::getArgs() !== null) + if($e->getPrevious() !== null) + { + // Check if previous is the same as the current + if($e->getPrevious()->getMessage() !== $e->getMessage()) + { + self::outExceptionDetails($e->getPrevious(), true); + } + } + + if(Main::getArgs() !== null && !$sub) { if(isset(Main::getArgs()['dbg-ex'])) { @@ -425,4 +496,53 @@ } } } + + /** + * Prompts for a password input while hiding the user's password + * + * @param string $prompt + * @return string|null + */ + public static function passwordInput(string $prompt): ?string + { + if(!ncc::cliMode()) + return null; + + // passwordInput() is not properly implemented yet, defaulting to prompt + return self::getInput($prompt); + + /** + $executable_finder = new ExecutableFinder(); + $bash_path = $executable_finder->find('bash'); + + if($bash_path == null) + { + self::outWarning('Unable to find bash executable, cannot hide password input'); + return self::getInput($prompt); + } + + $prompt = escapeshellarg($prompt); + $random = Functions::randomString(10); + $command = "$bash_path -c 'read -s -p $prompt $random && echo \$" . $random . "'"; + $password = rtrim(shell_exec($command)); + self::out((string)null); + return $password; + **/ + } + + /** + * @param array $sections + * @return void + */ + public static function outHelpSections(array $sections): void + { + if(!ncc::cliMode()) + return; + + $padding = Functions::detectParametersPadding($sections); + + foreach($sections as $section) + Console::out(' ' . $section->toString($padding)); + } + } \ No newline at end of file diff --git a/src/ncc/Utilities/Functions.php b/src/ncc/Utilities/Functions.php index fa4096c..863520b 100644 --- a/src/ncc/Utilities/Functions.php +++ b/src/ncc/Utilities/Functions.php @@ -1,26 +1,75 @@ Runner)) { + Runners::bash => BashRunner::processUnit($path, $policy), Runners::php => PhpRunner::processUnit($path, $policy), - default => throw new UnsupportedRunnerException('The runner \'' . $policy->Runner . '\' is not supported'), + Runners::perl => PerlRunner::processUnit($path, $policy), + Runners::python => PythonRunner::processUnit($path, $policy), + Runners::python2 => Python2Runner::processUnit($path, $policy), + Runners::python3 => Python3Runner::processUnit($path, $policy), + Runners::lua => LuaRunner::processUnit($path, $policy), + default => throw new RunnerExecutionException('The runner \'' . $policy->Runner . '\' is not supported'), }; } @@ -268,7 +323,7 @@ * @param Exception $e * @return array */ - public static function exceptionToArray(Exception $e): array + public static function exceptionToArray(Throwable $e): array { $exception = [ 'message' => $e->getMessage(), @@ -281,7 +336,7 @@ if($e->getPrevious() !== null) { - $exception['trace'] = self::exceptionToArray($e); + $exception['previous'] = self::exceptionToArray($e->getPrevious()); } return $exception; @@ -376,6 +431,7 @@ * @throws AccessDeniedException * @throws FileNotFoundException * @throws IOException + * @noinspection PhpUnused */ public static function loadComposerJson(string $path): ComposerJson { @@ -430,6 +486,7 @@ * * @param string $property * @return mixed|null + * @noinspection PhpMissingReturnTypeInspection */ public static function getConfigurationProperty(string $property) { @@ -452,4 +509,484 @@ return Parser::parse($version)->toString(); } + + /** + * Returns a random string + * + * @param int $length + * @return string + */ + public static function randomString(int $length = 32): string + { + $characters = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'; + $charactersLength = strlen($characters); + $randomString = ''; + for ($i = 0; $i < $length; $i++) + { + $randomString .= $characters[rand(0, $charactersLength - 1)]; + } + return $randomString; + } + + /** + * Returns a path to a temporary directory + * + * @param bool $create + * @param bool $set_as_tmp + * @return string + * @throws InvalidScopeException + */ + public static function getTmpDir(bool $create=true, bool $set_as_tmp=true): string + { + $path = PathFinder::getCachePath() . DIRECTORY_SEPARATOR . self::randomString(16); + if($create) + { + $filesystem = new Filesystem(); + /** @noinspection PhpRedundantOptionalArgumentInspection */ + $filesystem->mkdir($path, 0777); + } + if($set_as_tmp) + RuntimeCache::setFileAsTemporary($path); + return $path; + } + + /** + * Applies the authentication to the given HTTP request. + * + * @param HttpRequest $httpRequest + * @param Entry|null $entry + * @param bool $expect_json + * @return HttpRequest + * @throws AuthenticationException + * @throws GitlabServiceException + */ + public static function prepareGitServiceRequest(HttpRequest $httpRequest, ?Entry $entry=null, bool $expect_json=true): HttpRequest + { + if($entry !== null) + { + if (!$entry->isCurrentlyDecrypted()) + throw new GitlabServiceException('The given Vault entry is not decrypted.'); + + switch ($entry->getPassword()->getAuthenticationType()) { + case AuthenticationType::AccessToken: + $httpRequest->Headers[] = "Authorization: Bearer " . $entry->getPassword(); + break; + + case AuthenticationType::UsernamePassword: + throw new AuthenticationException('Username/Password authentication is not supported'); + } + } + + if($expect_json) + { + $httpRequest->Headers[] = "Accept: application/json"; + $httpRequest->Headers[] = "Content-Type: application/json"; + } + + return $httpRequest; + } + + /** + * Downloads a file from the given URL and saves it to the given path + * + * @param string $url + * @param Entry|null $entry + * @return string + * @throws AuthenticationException + * @throws GitlabServiceException + * @throws InvalidScopeException + * @throws HttpException + */ + public static function downloadGitServiceFile(string $url, ?Entry $entry=null): string + { + $out_path = Functions::getTmpDir() . "/" . basename($url); + + $httpRequest = new HttpRequest(); + $httpRequest->Url = $url; + $httpRequest->Type = HttpRequestType::GET; + $httpRequest = Functions::prepareGitServiceRequest($httpRequest, $entry, false); + + Console::out('Downloading file ' . $url); + HttpClient::download($httpRequest, $out_path); + + return $out_path; + } + + /** + * @param string $path + * @return string|null + * @throws ArchiveException + * @throws UnsupportedArchiveException + */ + public static function extractArchive(string $path): ?string + { + $executable_finder = new ExecutableFinder(); + $unzip_executable = $executable_finder->find('unzip'); + $tar_executable = $executable_finder->find('tar'); + $out_path = dirname($path); + $filesystem = new Filesystem(); + + if(!$filesystem->exists($out_path)) + $filesystem->mkdir($out_path); + + RuntimeCache::setFileAsTemporary($out_path); + + $mimeType = mime_content_type($path); + $supportedTypes = []; + + if($unzip_executable !== null) + { + $supportedTypes = array_merge($supportedTypes, [ + 'application/zip', + 'application/x-zip', + 'application/x-zip-compressed', + 'application/octet-stream', + 'application/x-compress', + 'application/x-compressed', + 'multipart/x-zip' + ]); + } + else + { + if(RuntimeCache::get('warning_zip_shown') !== true) + { + Console::out('unzip executable not found. ZIP archives will not be supported.'); + RuntimeCache::set('warning_zip_shown', true); + } + } + + if($tar_executable !== null) + { + $supportedTypes = array_merge($supportedTypes, [ + 'application/x-tar', + 'application/x-gzip', + 'application/x-bzip2', + 'application/x-xz' + ]); + } + else + { + if(RuntimeCache::get('warning_tar_shown') !== true) + { + Console::outWarning('tar executable not found. TAR archives will not be supported.'); + RuntimeCache::set('warning_tar_shown', true); + } + } + + if (!in_array($mimeType, $supportedTypes)) + throw new UnsupportedArchiveException("Unsupported archive type: $mimeType"); + + $command = match ($mimeType) { + 'application/zip' => [$unzip_executable, $path, '-d', $out_path], + 'application/x-tar' => [$tar_executable, '--verbose', '-xf', $path, '-C', $out_path], + 'application/x-gzip' => [$tar_executable, '--verbose', '-xzf', $path, '-C', $out_path], + 'application/x-bzip2' => [$tar_executable, '--verbose', '-xjf', $path, '-C', $out_path], + default => throw new UnsupportedArchiveException("Unsupported archive type: $mimeType"), + }; + + Console::out("Extracting archive $path"); + $process = new Process($command); + + // display the output of the command + $process->run(function ($type, $buffer) { + Console::outVerbose($buffer); + }); + + if (!$process->isSuccessful()) + throw new ArchiveException($process->getErrorOutput()); + + return $out_path; + } + + /** + * Scans the given directory for files and returns the found file + * + * @param string $path + * @param array $files + * @return string|null + */ + public static function searchDirectory(string $path, array $files): ?string + { + if (!is_dir($path)) + return null; + + // Search files in the given directory recursively + $iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($path)); + foreach ($iterator as $file) + { + if (in_array($file->getFilename(), $files)) + return $file->getPathname(); + } + + return null; + } + + /** + * Attempts to convert a weird version number to a standard version number + * + * @param $version + * @return string + */ + public static function convertToSemVer($version): string + { + if(stripos($version, 'v') === 0) + $version = substr($version, 1); + if(!Validate::version($version)) + { + $parts = explode('.', $version); + $major = (string)null; + $minor = (string)null; + $patch = (string)null; + + if(count($parts) >= 1) + $major = $parts[0]; + if(count($parts) >= 2) + $minor = $parts[1]; + if(count($parts) >= 3) + $patch = $parts[2]; + + // Assemble the SemVer compatible string + $version = "$major.$minor.$patch"; + } + if(!Validate::version($version)) + return '1.0.0'; + + return $version; + } + + /** + * Returns a complete RepositoryQueryResults object + * + * @param RemotePackageInput $packageInput + * @param DefinedRemoteSource $definedRemoteSource + * @param Entry|null $entry + * @return RepositoryQueryResults + */ + public static function getRepositoryQueryResults(RemotePackageInput $packageInput, DefinedRemoteSource $definedRemoteSource, ?Entry $entry): RepositoryQueryResults + { + $results = new RepositoryQueryResults(); + + switch($definedRemoteSource->Type) + { + case DefinedRemoteSourceType::Github: + $source = GithubService::class; + break; + + case DefinedRemoteSourceType::Gitlab: + $source = GitlabService::class; + break; + + default: + return $results; + } + + // Check if the specified version is a release + try + { + Console::outVerbose(sprintf('Attempting to fetch source code from %s', $definedRemoteSource->Host)); + $release_results = $source::getRelease($packageInput, $definedRemoteSource, $entry); + } + catch(Exception $e) + { + $release_results = null; + unset($e); + } + + // If the specified version is a release, download the source code + if($release_results !== null) + { + $results->ReleaseName = ($release_results->ReleaseName ?? null); + $results->ReleaseDescription = ($release_results->ReleaseDescription ?? null); + $results->Files = self::mergeFilesResults($release_results->Files, ($results->Files ?? null)); + if($release_results->Version !== null) + $results->Version = $release_results->Version; + } + + try + { + $git_results = $source::getGitRepository($packageInput, $definedRemoteSource, $entry); + } + catch(Exception $e) + { + $git_results = null; + unset($e); + } + + if($git_results !== null) + { + if($results->ReleaseName == null) + { + $results->ReleaseName = ($git_results->ReleaseName ?? null); + } + elseif($git_results->ReleaseName !== null) + { + if(strlen($git_results->ReleaseName) > strlen($results->ReleaseName)) + $results->ReleaseName = $git_results->ReleaseName; + } + + if($results->ReleaseDescription == null) + { + $results->ReleaseDescription = ($git_results->ReleaseDescription ?? null); + } + elseif($git_results->ReleaseDescription !== null) + { + if(strlen($git_results->ReleaseDescription) > strlen($results->ReleaseDescription)) + $results->ReleaseDescription = $git_results->ReleaseDescription; + } + + if($results->Version == null) + { + $results->Version = ($git_results->Version ?? null); + } + elseif($git_results->Version !== null) + { + // Version compare + if(VersionComparator::compareVersion($git_results->Version, $results->Version) > 0) + $results->Version = $git_results->Version; + } + + $results->Files = self::mergeFilesResults($git_results->Files, ($results->Files ?? null)); + } + + try + { + $ncc_package_results = $source::getNccPackage($packageInput, $definedRemoteSource, $entry); + } + catch(Exception $e) + { + unset($e); + $ncc_package_results = null; + } + + if($ncc_package_results !== null) + { + if($results->ReleaseName == null) + { + $results->ReleaseName = ($ncc_package_results->ReleaseName ?? null); + } + elseif($ncc_package_results->ReleaseName !== null) + { + if(strlen($ncc_package_results->ReleaseName) > strlen($results->ReleaseName)) + $results->ReleaseName = $ncc_package_results->ReleaseName; + } + + if($results->ReleaseDescription == null) + { + $results->ReleaseDescription = ($ncc_package_results->ReleaseDescription ?? null); + } + elseif($ncc_package_results->ReleaseDescription !== null) + { + if(strlen($ncc_package_results->ReleaseDescription) > strlen($results->ReleaseDescription)) + $results->ReleaseDescription = $ncc_package_results->ReleaseDescription; + } + + if($results->Version == null) + { + $results->Version = ($ncc_package_results->Version ?? null); + } + elseif($ncc_package_results->Version !== null) + { + // Version compare + if(VersionComparator::compareVersion($ncc_package_results->Version, $results->Version) > 0) + $results->Version = $ncc_package_results->Version; + } + + $results->Files = self::mergeFilesResults($ncc_package_results->Files, ($results->Files ?? null)); + } + + return $results; + } + + /** + * Merges the given Files object with another Files object + * + * @param Files $input + * @param Files|null $selected + * @return Files + */ + private static function mergeFilesResults(RepositoryQueryResults\Files $input, ?RepositoryQueryResults\Files $selected=null): RepositoryQueryResults\Files + { + if($selected == null) + $selected = new RepositoryQueryResults\Files(); + + if($input->GitSshUrl !== null) + $selected->GitSshUrl = $input->GitSshUrl; + + if($input->GitHttpUrl !== null) + $selected->GitHttpUrl = $input->GitHttpUrl; + + if($input->SourceUrl !== null) + $selected->SourceUrl = $input->SourceUrl; + + if($input->TarballUrl !== null) + $selected->TarballUrl = $input->TarballUrl; + + if($input->ZipballUrl !== null) + $selected->ZipballUrl = $input->ZipballUrl; + + if($input->PackageUrl !== null) + $selected->PackageUrl = $input->PackageUrl; + + return $selected; + } + + /** + * Attempts to cast the correct type of the given value + * + * @param string $input + * @return float|int|mixed|string + */ + public static function stringTypeCast(string $input): mixed + { + if (is_numeric($input)) + { + if (str_contains($input, '.')) + return (float)$input; + + if (ctype_digit($input)) + return (int)$input; + } + elseif (in_array(strtolower($input), ['true', 'false'])) + { + return filter_var($input, FILTER_VALIDATE_BOOLEAN); + } + + return (string)$input; + } + + /** + * Finalizes the permissions + * + * @return void + * @throws InvalidScopeException + */ + public static function finalizePermissions(): void + { + if(Resolver::resolveScope() !== Scopes::System) + return; + + Console::outVerbose('Finalizing permissions...'); + $filesystem = new Filesystem(); + + try + { + if($filesystem->exists(PathFinder::getDataPath(Scopes::System))) + $filesystem->chmod(PathFinder::getDataPath(Scopes::System), 0777, 0000, true); + } + catch(Exception $e) + { + Console::outWarning(sprintf('Failed to finalize permissions for %s: %s', PathFinder::getDataPath() . DIRECTORY_SEPARATOR . 'data', $e->getMessage())); + } + + try + { + if($filesystem->exists(PathFinder::getCachePath(Scopes::System))) + $filesystem->chmod(PathFinder::getCachePath(Scopes::System), 0777, 0000, true); + } + catch(Exception $e) + { + Console::outWarning(sprintf('Failed to finalize permissions for %s: %s', PathFinder::getDataPath() . DIRECTORY_SEPARATOR . 'data', $e->getMessage())); + } + + } } \ No newline at end of file diff --git a/src/ncc/Utilities/IO.php b/src/ncc/Utilities/IO.php index 7b9c5be..13d76ea 100644 --- a/src/ncc/Utilities/IO.php +++ b/src/ncc/Utilities/IO.php @@ -1,6 +1,26 @@ find($runner); + + if($exec_path !== null) + return $exec_path; + + throw new RunnerExecutionException(sprintf('Unable to find \'%s\' executable', $runner)); + } } \ No newline at end of file diff --git a/src/ncc/Utilities/Resolver.php b/src/ncc/Utilities/Resolver.php index 4d172a0..c58edab 100644 --- a/src/ncc/Utilities/Resolver.php +++ b/src/ncc/Utilities/Resolver.php @@ -1,11 +1,36 @@ $match) @@ -241,4 +266,64 @@ return false; } } + + /** + * Detects the remote source type, can also accept defined remote + * sources as the input, the function will look for the source + * type and return it + * + * @param string $input + * @return string + */ + public static function detectRemoteSourceType(string $input): string + { + if(in_array($input, BuiltinRemoteSourceType::All)) + return RemoteSourceType::Builtin; + + $source_manager = new RemoteSourcesManager(); + $defined_source = $source_manager->getRemoteSource($input); + if($defined_source == null) + return RemoteSourceType::Unknown; + + return RemoteSourceType::Defined; + } + + /** + * Detects the project type from the specified path + * + * @param string $path + * @return ProjectDetectionResults + */ + public static function detectProjectType(string $path): ProjectDetectionResults + { + $project_files = [ + 'project.json', + 'composer.json' + ]; + + $project_file = Functions::searchDirectory($path, $project_files); + + $project_detection_results = new ProjectDetectionResults(); + $project_detection_results->ProjectType = ProjectType::Unknown; + + if($project_file == null) + { + return $project_detection_results; + } + + // Get filename of the project file + switch(basename($project_file)) + { + case 'project.json': + $project_detection_results->ProjectType = ProjectType::Ncc; + break; + + case 'composer.json': + $project_detection_results->ProjectType = ProjectType::Composer; + break; + } + + $project_detection_results->ProjectPath = dirname($project_file); + return $project_detection_results; + } } \ No newline at end of file diff --git a/src/ncc/Utilities/RuntimeCache.php b/src/ncc/Utilities/RuntimeCache.php index 9aad37e..f06ada3 100644 --- a/src/ncc/Utilities/RuntimeCache.php +++ b/src/ncc/Utilities/RuntimeCache.php @@ -1,7 +1,30 @@ remove($file); + Console::outDebug(sprintf('deleted temporary file \'%s\'', $file)); + } + catch (Exception $e) + { + Console::outDebug(sprintf('failed to delete temporary file \'%s\', %s', $file, $e->getMessage())); + unset($e); + } } } } diff --git a/src/ncc/Utilities/Security.php b/src/ncc/Utilities/Security.php index 5e4c4cc..f2d3de2 100644 --- a/src/ncc/Utilities/Security.php +++ b/src/ncc/Utilities/Security.php @@ -1,6 +1,26 @@ ; +chomp($name); +print("Hello, $name\n"); \ No newline at end of file diff --git a/tests/example_project/scripts/unit.py2 b/tests/example_project/scripts/unit.py2 new file mode 100644 index 0000000..af4b858 --- /dev/null +++ b/tests/example_project/scripts/unit.py2 @@ -0,0 +1,3 @@ +print('Hello World!') +name = input('What is your name? ') +print('Hello', name) \ No newline at end of file diff --git a/tests/example_project/scripts/unit.py3 b/tests/example_project/scripts/unit.py3 new file mode 100644 index 0000000..af4b858 --- /dev/null +++ b/tests/example_project/scripts/unit.py3 @@ -0,0 +1,3 @@ +print('Hello World!') +name = input('What is your name? ') +print('Hello', name) \ No newline at end of file