Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions bin/baton.bat
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
@echo off
setlocal DISABLEDELAYEDEXPANSION

php "%~dp0baton" %*
exit /b %ERRORLEVEL%
5 changes: 5 additions & 0 deletions docs/project-inventory.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@ fingerprints. It may contain canonical local roots needed to read sources, but
never source bodies, credentials, environment values, processor logs, or cache
implementation keys.

Tooling inventories include every declared binary entry in addition to the
autoloaded and generated package surface. Build and run inventories remain
target-specific; this broader source set exists so editor analysis can resolve
symbols across entry files and reusable declarations.

Packages without dependencies do not require `Baton.lock`. Their lock fingerprint
is the SHA-256 digest of JSON `null`, so creating or removing a lock invalidates the
project snapshot without making lockless packages invalid.
Expand Down
16 changes: 15 additions & 1 deletion packaging/php-runtime/build.php
Original file line number Diff line number Diff line change
Expand Up @@ -266,7 +266,7 @@ function hostTarget(): string
'Windows' => 'windows',
default => fail('Unsupported build host: ' . PHP_OS_FAMILY, EXIT_USAGE),
};
$machine = strtolower(php_uname('m'));
$machine = strtolower(hostMachine());
$architecture = match ($machine) {
'x86_64', 'amd64' => 'x86_64',
'aarch64', 'arm64' => 'aarch64',
Expand All @@ -280,6 +280,20 @@ function hostTarget(): string
return "{$platform}-{$architecture}";
}

function hostMachine(): string
{
if (PHP_OS_FAMILY === 'Windows') {
foreach (['PROCESSOR_ARCHITEW6432', 'PROCESSOR_ARCHITECTURE'] as $name) {
$value = getenv($name);
if (is_string($value) && $value !== '') {
return $value;
}
}
}

return php_uname('m');
}

function absolutePath(string $path, string $base): string
{
if (preg_match('~^(?:[A-Za-z]:[\\\\/]|/)~', $path) === 1) {
Expand Down
46 changes: 46 additions & 0 deletions src/Commands/RunCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,10 @@ private function forwardBuildFailure(OutputInterface $output): void
/** @param list<string> $arguments */
private function runArtifact(string $artifact, array $arguments, string $projectRoot): int
{
if (PHP_OS_FAMILY === 'Windows') {
return $this->runWindowsArtifact($artifact, $arguments, $projectRoot);
}

$process = new Process(
[$artifact, ...$arguments],
$projectRoot,
Expand Down Expand Up @@ -180,4 +184,46 @@ private function runArtifact(string $artifact, array $arguments, string $project
);
}
}

/**
* Symfony's input pump cannot reliably make a Windows console stream
* nonblocking. Passing STDIN as Process input can therefore pause here
* until the user presses a key even when the program never reads stdin.
* Direct descriptor inheritance preserves interactive input without a
* parent-side read loop.
*
* @param list<string> $arguments
*/
private function runWindowsArtifact(string $artifact, array $arguments, string $projectRoot): int
{
$descriptors = [
0 => ['file', 'php://stdin', 'r'],
1 => ['file', 'php://stdout', 'w'],
2 => ['file', 'php://stderr', 'w'],
];
$pipes = [];
$process = @proc_open(
[$artifact, ...$arguments],
$descriptors,
$pipes,
$projectRoot,
);
if (!is_resource($process)) {
throw new BatonError(
'B0404',
'Built Program Could Not Be Started',
"Failed to run:\n {$artifact}",
['Rebuild the artifact, then run it again:'],
['baton build', 'baton run'],
);
}

foreach ($pipes as $pipe) {
if (is_resource($pipe)) {
fclose($pipe);
}
}

return proc_close($process);
}
}
15 changes: 4 additions & 11 deletions src/Project/ProjectDocumentBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ public function build(ProjectSelection $selection, bool $development, NetworkPol
[$rootPackage, $resolvedGraph] = $this->ensureRootPackage($rootMember, $resolvedGraph, $selected);
$visible = $this->visiblePackages($rootPackage, $resolvedGraph, $development, $selection->aggregate);
$registry = $this->generatedRegistry($selection, $visible, $resolvedGraph->packages);
[$rootPackage, $resolvedGraph, $visible] = $this->withGeneratedSources(
[$rootPackage, $resolvedGraph, $visible] = $this->withToolingSources(
$rootPackage,
$resolvedGraph,
$visible,
Expand Down Expand Up @@ -179,7 +179,7 @@ private function ensureRootPackage(
$root->manifest,
new ResolvedPackageSource('workspace', $root->root, $root->relativePath),
(new ManifestFingerprint())->calculate($root->manifest),
(new SourceDiscovery($root->root))->discover($root->manifest, $selected),
(new SourceDiscovery($root->root))->discoverForTooling($root->manifest),
);
$packages = $graph->packages;
$packages[$root->manifest->package->name] = $package;
Expand Down Expand Up @@ -234,7 +234,7 @@ private function generatedRegistry(ProjectSelection $selection, array $visible,
* @param array<string, list<GeneratedSourceInput>> $generated
* @return array{ResolvedPackage, ResolvedDependencyGraph|ResolvedWorkspaceGraph, array<string, ResolvedPackage>}
*/
private function withGeneratedSources(
private function withToolingSources(
ResolvedPackage $root,
ResolvedDependencyGraph|ResolvedWorkspaceGraph $graph,
array $visible,
Expand All @@ -243,25 +243,18 @@ private function withGeneratedSources(
bool $development,
bool $aggregate,
): array {
if ($generated === []) {
return [$root, $graph, $visible];
}
$packages = $graph->packages;
foreach ($visible as $name => $package) {
$inputs = $generated[$name] ?? [];
if ($inputs === []) {
continue;
}
$target = $name === $root->manifest->package->name
? $rootTarget
: new SelectedPackageTarget(
$package->manifest->targets->library
?? ($package->manifest->targets->binaries[0]
?? throw new \LogicException('A source-visible package must declare a target.')),
);
$inventory = (new SourceDiscovery($package->source->root))->discover(
$inventory = (new SourceDiscovery($package->source->root))->discoverForTooling(
$package->manifest,
$target,
$inputs,
);
$replacement = $package->withInventory($target, $inventory, true);
Expand Down
49 changes: 44 additions & 5 deletions src/Source/SourceDiscovery.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,49 @@ public function discover(
Schema2Manifest $manifest,
SelectedPackageTarget $selected,
array $generatedSources = [],
): SourceInventory {
$entries = [];
if ($selected->isBinary()) {
$entry = $selected->entry();
if ($entry === null) {
throw $this->error('Binary Target Entry Is Missing', '', 'The selected binary has no entry source.');
}
$entries[] = $entry;
}

return $this->discoverSources($manifest, $entries, $generatedSources);
}

/**
* Returns the complete package surface needed by language tooling rather than
* the source set of one executable target.
*
* @param list<GeneratedSourceInput> $generatedSources
*/
public function discoverForTooling(
Schema2Manifest $manifest,
array $generatedSources = [],
): SourceInventory {
$entries = [];
foreach ($manifest->targets->binaries as $binary) {
$entries[$this->normalize($binary->entryPath)] = $binary->entryPath;
}

return $this->discoverSources(
$manifest,
array_values($entries),
$generatedSources,
);
}

/**
* @param list<string> $includedEntries
* @param list<GeneratedSourceInput> $generatedSources
*/
private function discoverSources(
Schema2Manifest $manifest,
array $includedEntries,
array $generatedSources,
): SourceInventory {
$entryPaths = [];
foreach ($manifest->targets->binaries as $binary) {
Expand Down Expand Up @@ -70,11 +113,7 @@ public function discover(
}
}

if ($selected->isBinary()) {
$entry = $selected->entry();
if ($entry === null) {
throw $this->error('Binary Target Entry Is Missing', '', 'The selected binary has no entry source.');
}
foreach ($includedEntries as $entry) {
$resolved = $this->paths->file($entry);
$contents = @file_get_contents($resolved->canonicalPath);
if ($contents === false || preg_match('//u', $contents) !== 1) {
Expand Down
58 changes: 49 additions & 9 deletions src/Testing/RuntimeOutcomeReader.php
Original file line number Diff line number Diff line change
Expand Up @@ -436,21 +436,61 @@ public function u64(): int

public static function unsigned64(string $bytes): int
{
/** @var array{value: int} $value */
$value = unpack('Pvalue', $bytes);
if ($value['value'] < 0) {
throw new RuntimeOutcomeInvalid('Runtime outcome integer exceeds the host transport range.');
return self::decodeUnsigned64($bytes);
}

public static function signed64(string $bytes): int
{
self::require64Bits($bytes);
if ((ord($bytes[7]) & 0x80) === 0) {
return self::decodeUnsigned64($bytes);
}

return $value['value'];
$minimum = str_repeat("\0", PHP_INT_SIZE - 1)
. "\x80"
. str_repeat("\xff", 8 - PHP_INT_SIZE);
if ($bytes === $minimum) {
return PHP_INT_MIN;
}

$magnitude = '';
$carry = 1;
for ($index = 0; $index < 8; ++$index) {
$byte = 255 - ord($bytes[$index]) + $carry;
if ($byte === 256) {
$byte = 0;
$carry = 1;
} else {
$carry = 0;
}
$magnitude .= chr($byte);
}

return -self::decodeUnsigned64($magnitude);
}

public static function signed64(string $bytes): int
private static function decodeUnsigned64(string $bytes): int
{
/** @var array{value: int} $value */
$value = unpack('Pvalue', $bytes);
self::require64Bits($bytes);
$value = 0;
for ($index = 7; $index >= 0; --$index) {
$byte = ord($bytes[$index]);
if ($value > intdiv(PHP_INT_MAX - $byte, 256)) {
throw new RuntimeOutcomeInvalid(
'Runtime outcome integer exceeds the host transport range.',
);
}
$value = ($value * 256) + $byte;
}

return $value['value'];
return $value;
}

private static function require64Bits(string $bytes): void
{
if (strlen($bytes) !== 8) {
throw new RuntimeOutcomeInvalid('Runtime outcome integer is not 64 bits wide.');
}
}

public function text(int $length): string
Expand Down
34 changes: 32 additions & 2 deletions src/Toolchain/Platform.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,17 @@ public function __construct(
) {
}

public static function host(?string $osFamily = null, ?string $machine = null): self
/**
* @param array<string, string>|null $environment
*/
public static function host(
?string $osFamily = null,
?string $machine = null,
?array $environment = null,
): self
{
$osFamily ??= PHP_OS_FAMILY;
$machine ??= php_uname('m');
$machine ??= self::hostMachine($osFamily, $environment);

$name = match (strtolower($osFamily)) {
'windows' => 'windows',
Expand Down Expand Up @@ -48,6 +55,29 @@ public static function host(?string $osFamily = null, ?string $machine = null):
return new self($name, $architecture);
}

/**
* A 32-bit PHP process reports i586 on 64-bit Windows. In that case,
* PROCESSOR_ARCHITEW6432 identifies the native host architecture.
*
* @param array<string, string>|null $environment
*/
private static function hostMachine(string $osFamily, ?array $environment): string
{
if (strtolower($osFamily) === 'windows') {
foreach (['PROCESSOR_ARCHITEW6432', 'PROCESSOR_ARCHITECTURE'] as $name) {
$value = $environment === null
? getenv($name)
: ($environment[$name] ?? false);

if (is_string($value) && $value !== '') {
return $value;
}
}
}

return php_uname('m');
}

public function target(): string
{
return "{$this->name}-{$this->architecture}";
Expand Down
36 changes: 36 additions & 0 deletions tests/Unit/PlatformTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?php

declare(strict_types=1);

namespace Doria\Baton\Tests\Unit;

use Doria\Baton\Tests\TestCase;
use Doria\Baton\Toolchain\Platform;

final class PlatformTest extends TestCase
{
public function testUsesNativeAmd64ArchitectureForA32BitProcessOnWindows(): void
{
$host = Platform::host(
osFamily: 'Windows',
environment: [
'PROCESSOR_ARCHITECTURE' => 'x86',
'PROCESSOR_ARCHITEW6432' => 'AMD64',
],
);

self::assertSame('windows', $host->name);
self::assertSame('x86_64', $host->architecture);
self::assertSame('windows-x86_64', $host->target());
}

public function testUsesNativeArm64ArchitectureOnWindows(): void
{
$host = Platform::host(
osFamily: 'Windows',
environment: ['PROCESSOR_ARCHITECTURE' => 'ARM64'],
);

self::assertSame('windows-aarch64', $host->target());
}
}
Loading
Loading