ncc/src/ncc/Classes/PhpExtension/ExecutableCompiler.php

216 lines
No EOL
8.8 KiB
PHP

<?php
/*
* Copyright (c) Nosial 2022-2023, 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:
*
* 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 NON-INFRINGEMENT. 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.
*
*/
namespace ncc\Classes\PhpExtension;
use ncc\Classes\NccExtension\ConstantCompiler;
use ncc\CLI\Main;
use ncc\Enums\LogLevel;
use ncc\Enums\Options\BuildConfigurationOptions;
use ncc\Enums\Options\BuildConfigurationValues;
use ncc\Exceptions\BuildException;
use ncc\ThirdParty\Symfony\Process\ExecutableFinder;
use ncc\ThirdParty\Symfony\Process\Process;
use ncc\Utilities\Console;
use ncc\Utilities\ConsoleProgressBar;
use ncc\Utilities\Functions;
use ncc\Utilities\PathFinder;
class ExecutableCompiler extends NccCompiler
{
/**
* @inheritDoc
* @throws BuildException
*/
public function build(string $build_configuration = BuildConfigurationValues::DEFAULT->value, array $options=[]): string
{
$configuration = $this->getProjectManager()->getProjectConfiguration()->getBuild()->getBuildConfiguration($build_configuration);
if(count($options) > 0)
{
$configuration->setOptions(array_merge($configuration->getOptions(), $options));
}
if(!isset($configuration->getOptions()[BuildConfigurationOptions::NCC_CONFIGURATION->value]))
{
throw new BuildException(sprintf("Unable to compile the binary, the build configuration '%s' does not have a ncc_configuration.", $build_configuration));
}
// Build the ncc package first
Console::outVerbose('Building ncc package.');
$ncc_package = parent::build($configuration->getOptions()[BuildConfigurationOptions::NCC_CONFIGURATION->value]);
// Prepare the ncc package for compilation
$hex_dump_file = PathFinder::getCachePath() . DIRECTORY_SEPARATOR . $this->getProjectManager()->getProjectConfiguration()->getAssembly()->getName() . '.c';
if(is_file($hex_dump_file))
{
unlink($hex_dump_file);
}
Console::outVerbose(sprintf('Converting ncc package %s to hex dump', $ncc_package));
$this->hexDump($ncc_package, $hex_dump_file, $this->getProjectManager()->getProjectConfiguration()->getAssembly()->getName());
// Prepare the gcc command
$gcc_path = (new ExecutableFinder())->find('gcc');
if(isset($configuration->getOptions()[BuildConfigurationOptions::OUTPUT_FILE->value]))
{
$binary_path = ConstantCompiler::compileConstants(
$this->getProjectManager()->getProjectConfiguration(),
$configuration->getOptions()[BuildConfigurationOptions::OUTPUT_FILE->value]
);
}
else
{
$binary_path = ConstantCompiler::compileConstants($this->getProjectManager()->getProjectConfiguration(), $configuration->getOutput());
}
if($gcc_path === null)
{
throw new BuildException("Unable to find gcc executable, please make sure it is installed and in your PATH environment variable.");
}
if(!is_file(__DIR__ . DIRECTORY_SEPARATOR . 'bootstrap_main.c'))
{
throw new BuildException("Unable to find bootstrap_main.c, please make sure ncc is installed correctly.");
}
$gcc_options = [
__DIR__ . DIRECTORY_SEPARATOR . 'bootstrap_main.c',
realpath($hex_dump_file)
];
foreach($configuration->getOptions() as $option => $value)
{
if(str_starts_with($option, 'gcc-'))
{
$gcc_options[] = sprintf('-%s%s', substr($option, 4), $value === null ? '' : '=' . $value);
}
}
$gcc_options[] = '-o';
$gcc_options[] = $binary_path;
switch(Main::getLogLevel())
{
case LogLevel::VERBOSE->value:
$gcc_options[] = '-v';
break;
case LogLevel::DEBUG->value:
$gcc_options[] = '-v';
$gcc_options[] = '-v';
break;
}
$process = new Process([$gcc_path, ...$gcc_options]);
$process->setTimeout(0);
Console::outVerbose(sprintf('Compiling executable to %s: %s', $binary_path, implode(' ', $gcc_options)));
$process->run(static function ($type, $buffer)
{
// If $buffer contains multiple lines, split it and output each line separately
if(str_contains($buffer, "\n"))
{
foreach(explode("\n", $buffer) as $line)
{
if($line === '')
{
continue;
}
Console::outVerbose(rtrim($line, "\n"));
}
return;
}
Console::outVerbose(rtrim($buffer, "\n"));
});
if(!$process->isSuccessful())
{
unlink($hex_dump_file);
throw new BuildException(sprintf("Unable to compile the binary, gcc exited with code %d: %s", $process->getExitCode(), $process->getErrorOutput()));
}
// Finally, remove the hex dump file and return the executable path
unlink($hex_dump_file);
return $binary_path;
}
/**
* Creates a hex dump of the binary data and writes it to the output file suitable for inclusion in a C source
* file, this is a similar utility to xxd.
*
* @param string $input_path
* @param string $output_path
* @param string $variable_name
* @return void
*/
private function hexDump(string $input_path, string $output_path, string $variable_name): void
{
Console::outVerbose(sprintf('Processing %s to hex dump', $input_path));
$input = fopen($input_path, 'rb');
$output = fopen($output_path, 'wb');
$byte_count = 0;
$progress_bar = new ConsoleProgressBar(sprintf('HexDump %s', $input_path), filesize($input_path));
fwrite($output, sprintf("unsigned char %s[] = {\n", Functions::toSnakeCase($variable_name)));
// Convert the binary data to hex and write it to the output file using chunks
while (!feof($input))
{
$bytes = fread($input, 5026);
$len = strlen($bytes);
for ($i = 0; $i < $len; $i++)
{
fwrite($output, sprintf(" 0x%02x,", ord($bytes[$i])));
$byte_count++;
// Write 12 bytes per line or when reaching the end of the file
if ($byte_count === 12 || ($i == $len - 1 && feof($input)))
{
fwrite($output, "\n");
$byte_count = 0;
}
}
$progress_bar->increaseValue($len, true);
$progress_bar->setMiscText(sprintf('Processed (%d/%d)', $progress_bar->getValue(), $progress_bar->getMaxValue()));
}
// Close the output file
fseek($output, -2, SEEK_END);
fwrite($output, "\n");
fwrite($output, "};\n");
// Finally, close the input and output files
fclose($input);
fclose($output);
// Close the progress bar
$progress_bar->setMiscText('done', true);
unset($progress_bar);
}
}