Skip to content
Open
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
66 commits
Select commit Hold shift + click to select a range
8820a26
feat: Windows support
Jan 9, 2026
a5b6be8
fix tests
Jan 10, 2026
9b214e6
revert changes to watcher_test.go
dunglas Jan 10, 2026
0d67a8f
cs
dunglas Jan 10, 2026
29bf6df
fix remaining failing tests
Jan 10, 2026
58f55ef
fix watcher support
Jan 11, 2026
d91e8ba
cleanup
Jan 11, 2026
cc1be56
improve watcher tests (wip)
dunglas Jan 12, 2026
5b39fa8
all tests are green now!
Jan 12, 2026
4799c3a
cleanup
Jan 12, 2026
5da1dc0
test forward slashes on Windows
Jan 12, 2026
12585fd
upgrade watcher to simplify Windows linking
Jan 12, 2026
25d9a98
add php-cli support
Jan 12, 2026
c9ab80e
review
Jan 12, 2026
4bbe362
GitHub Actions worklow
dunglas Jan 13, 2026
f08f4bb
wip
dunglas Jan 15, 2026
6bab64f
work on windows workflow
henderkes Jan 16, 2026
36e3492
linter
henderkes Jan 16, 2026
684b5e0
fix env var
dunglas Jan 16, 2026
25b4b9f
use xcaddy
henderkes Jan 16, 2026
bb3fb4e
correct path
henderkes Jan 16, 2026
4aa0cbb
remove tmate action
henderkes Jan 16, 2026
d720ab1
add caddy modules back in
henderkes Jan 16, 2026
a7ed504
remove extra v
henderkes Jan 16, 2026
2e71f67
brotlidec missing
henderkes Jan 16, 2026
734203f
wip
dunglas Jan 16, 2026
4946a17
wip
dunglas Jan 16, 2026
63103cb
with GOFLAGS
dunglas Jan 16, 2026
7c4507a
wip
dunglas Jan 16, 2026
8136c69
wip
dunglas Jan 16, 2026
4bf3e92
wip
dunglas Jan 16, 2026
45b5bfe
don't use xcaddy
dunglas Jan 16, 2026
5c1fe58
fix Caddy module tests
Jan 17, 2026
aa2d96e
-X CustomVersion is part of -ldflags
henderkes Jan 17, 2026
75615e5
fix
henderkes Jan 17, 2026
2dbed0a
don't forget to add FRANKENPHP_VERSION to CGO_CFLAGS, otherwise phpin…
henderkes Jan 17, 2026
7e8e250
why is quoting so hard in powershell
henderkes Jan 17, 2026
6f2745d
permanently change it
henderkes Jan 17, 2026
3a66f36
seems impossible to do with GOFLAGS
henderkes Jan 17, 2026
efbd362
woopsie
henderkes Jan 17, 2026
ebb4fbd
fix \n vs \r\n issues
henderkes Jan 17, 2026
7e75699
fix double zipping, re-enable compression (actually makes a big diffe…
henderkes Jan 17, 2026
5645b66
fix worker match
Jan 23, 2026
c83af6f
Merge branch 'windows' of https://github.com/php/frankenphp into windows
Jan 23, 2026
fb9ff0a
add frankenphp icon for windows .exe
henderkes Feb 6, 2026
c0a3ffc
use goversioninfo to embed icon and metadata
henderkes Feb 6, 2026
05cdf29
Merge branch 'main' into windows
henderkes Feb 6, 2026
e01a08e
remove unnecessary moving of watcher-c.h file
henderkes Feb 6, 2026
5482bbb
go mod tidy
henderkes Feb 6, 2026
79db4c0
goversioninfo param
henderkes Feb 6, 2026
451c96f
throw AI at how to create a file without BOM in powershell
henderkes Feb 6, 2026
e522dfc
fix file path to icon
henderkes Feb 6, 2026
13de7c3
use the latest windows release, apparently the php hosted one is out …
henderkes Feb 6, 2026
3697e0c
use malloc/free because I don't want to mess with linking against php…
henderkes Feb 6, 2026
71f5b56
Revert "use malloc/free because I don't want to mess with linking aga…
henderkes Feb 6, 2026
247de71
maybe?
henderkes Feb 6, 2026
158c689
use malloc/free instead @dunglas revert this
henderkes Feb 6, 2026
c2aab04
explicitly utf8 no bom
henderkes Feb 6, 2026
376635f
use cover as icon
henderkes Feb 6, 2026
b084121
attempt to encode é character
henderkes Feb 6, 2026
590658a
hail mary
henderkes Feb 6, 2026
c1e5a21
oops
henderkes Feb 6, 2026
06322ac
god I hate windows BOM
henderkes Feb 6, 2026
7779485
at least it doesn't fail the job, even though it doesn't correctly en…
henderkes Feb 6, 2026
db995d2
try windows-1252
henderkes Feb 6, 2026
cce64a6
linter
henderkes Feb 7, 2026
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 caddy/frankenphp/cbrotli.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
//go:build !nobrotli

package main

import _ "github.com/dunglas/caddy-cbrotli"
1 change: 0 additions & 1 deletion caddy/frankenphp/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import (

// plug in Caddy modules here.
_ "github.com/caddyserver/caddy/v2/modules/standard"
_ "github.com/dunglas/caddy-cbrotli"
_ "github.com/dunglas/frankenphp/caddy"
_ "github.com/dunglas/mercure/caddy"
_ "github.com/dunglas/vulcain/caddy"
Expand Down
2 changes: 2 additions & 0 deletions caddy/php-cli.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
//go:build !windows

package caddy

import (
Expand Down
2 changes: 1 addition & 1 deletion cgi.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ package frankenphp
// #cgo noescape frankenphp_register_variables_from_request_info
// #cgo noescape frankenphp_register_variable_safe
// #cgo noescape frankenphp_register_single
// #include <php_variables.h>
// #include "frankenphp.h"
// #include <php_variables.h>
import "C"
import (
"context"
Expand Down
6 changes: 4 additions & 2 deletions cgo.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
package frankenphp

// #cgo darwin pkg-config: libxml-2.0
// #cgo CFLAGS: -Wall -Werror
// #cgo unix CFLAGS: -Wall -Werror
// #cgo linux CFLAGS: -D_GNU_SOURCE
// #cgo LDFLAGS: -lphp -lm -lutil
// #cgo unix LDFLAGS: -lphp -lm -lutil
// #cgo linux LDFLAGS: -ldl -lresolv
// #cgo darwin LDFLAGS: -Wl,-rpath,/usr/local/lib -liconv -ldl
// #cgo windows CFLAGS: -D_WINDOWS -DWINDOWS=1 -DZEND_WIN32=1 -DPHP_WIN32=1 -DWIN32 -D_MBCS -D_USE_MATH_DEFINES -DNDebug -DNDEBUG -DZEND_DEBUG=0 -DZTS=1 -DFD_SETSIZE=256
// #cgo windows LDFLAGS: -lpthreadVC3
import "C"
35 changes: 35 additions & 0 deletions cli.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
//go:build !windows

// TODO: ignored on Windows for now (even if it should work with a custom PHP build),
// because static builds of the embed SAPI aren't available yet and php.exe is ship with
// the standard PHP distribution.

package frankenphp

// #include "frankenphp.h"
import "C"
import "unsafe"

// ExecuteScriptCLI executes the PHP script passed as parameter.
// It returns the exit status code of the script.
func ExecuteScriptCLI(script string, args []string) int {
// Ensure extensions are registered before CLI execution
registerExtensions()

cScript := C.CString(script)
defer C.free(unsafe.Pointer(cScript))

argc, argv := convertArgs(args)
defer freeArgs(argv)

return int(C.frankenphp_execute_script_cli(cScript, argc, (**C.char)(unsafe.Pointer(&argv[0])), false))
}

func ExecutePHPCode(phpCode string) int {
// Ensure extensions are registered before CLI execution
registerExtensions()

cCode := C.CString(phpCode)
defer C.free(unsafe.Pointer(cCode))
return int(C.frankenphp_execute_script_cli(cCode, 0, nil, true))
}
57 changes: 57 additions & 0 deletions cli_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
//go:build !windows

package frankenphp_test

import (
"errors"
"log"
"os"
"os/exec"
"testing"

"github.com/dunglas/frankenphp"
"github.com/stretchr/testify/assert"
)

func TestExecuteScriptCLI(t *testing.T) {
if _, err := os.Stat("internal/testcli/testcli"); err != nil {
t.Skip("internal/testcli/testcli has not been compiled, run `cd internal/testcli/ && go build`")
}

cmd := exec.Command("internal/testcli/testcli", "testdata/command.php", "foo", "bar")
stdoutStderr, err := cmd.CombinedOutput()
assert.Error(t, err)

var exitError *exec.ExitError
if errors.As(err, &exitError) {
assert.Equal(t, 3, exitError.ExitCode())
}

stdoutStderrStr := string(stdoutStderr)

assert.Contains(t, stdoutStderrStr, `"foo"`)
assert.Contains(t, stdoutStderrStr, `"bar"`)
assert.Contains(t, stdoutStderrStr, "From the CLI")
}

func TestExecuteCLICode(t *testing.T) {
if _, err := os.Stat("internal/testcli/testcli"); err != nil {
t.Skip("internal/testcli/testcli has not been compiled, run `cd internal/testcli/ && go build`")
}

cmd := exec.Command("internal/testcli/testcli", "-r", "echo 'Hello World';")
stdoutStderr, err := cmd.CombinedOutput()
assert.NoError(t, err)

stdoutStderrStr := string(stdoutStderr)
assert.Equal(t, stdoutStderrStr, `Hello World`)
}

func ExampleExecuteScriptCLI() {
if len(os.Args) <= 1 {
log.Println("Usage: my-program script.php")
os.Exit(1)
}

os.Exit(frankenphp.ExecuteScriptCLI(os.Args[1], os.Args))
}
2 changes: 1 addition & 1 deletion ext.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package frankenphp

//#include "frankenphp.h"
// #include "frankenphp.h"
import "C"
import (
"sync"
Expand Down
16 changes: 13 additions & 3 deletions frankenphp.c
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
#include "frankenphp.h"
#include <SAPI.h>
#include <Zend/zend_alloc.h>
#include <Zend/zend_exceptions.h>
#include <Zend/zend_interfaces.h>
#include <Zend/zend_types.h>
#include <errno.h>
#include <ext/spl/spl_exceptions.h>
#include <ext/standard/head.h>
#include <inttypes.h>
#include <php.h>
#ifdef PHP_WIN32
#include <config.w32.h>
#else
#include <php_config.h>
#endif
#include <php_ini.h>
#include <php_main.h>
#include <php_output.h>
Expand All @@ -19,7 +23,9 @@
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#ifndef ZEND_WIN32
#include <unistd.h>
#endif
#if defined(__linux__)
#include <sys/prctl.h>
#elif defined(__FreeBSD__) || defined(__OpenBSD__)
Expand Down Expand Up @@ -205,7 +211,7 @@ bool frankenphp_shutdown_dummy_request(void) {
return true;
}

PHPAPI void get_full_env(zval *track_vars_array) {
void get_full_env(zval *track_vars_array) {
go_getfullenv(thread_index, track_vars_array);
}

Expand Down Expand Up @@ -959,6 +965,7 @@ static void *php_thread(void *arg) {
}

static void *php_main(void *arg) {
#ifndef ZEND_WIN32
/*
* SIGPIPE must be masked in non-Go threads:
* https://pkg.go.dev/os/signal#hdr-Go_programs_that_use_cgo_or_SWIG
Expand All @@ -971,6 +978,7 @@ static void *php_main(void *arg) {
perror("failed to block SIGPIPE");
exit(EXIT_FAILURE);
}
#endif

set_thread_name("php-main");

Expand Down Expand Up @@ -1188,6 +1196,7 @@ static void sapi_cli_register_variables(zval *track_vars_array) /* {{{ */
}
/* }}} */

#ifndef ZEND_WIN32
static void *execute_script_cli(void *arg) {
void *exit_status;
bool eval = (bool)arg;
Expand Down Expand Up @@ -1249,6 +1258,7 @@ int frankenphp_execute_script_cli(char *script, int argc, char **argv,

return (intptr_t)exit_status;
}
#endif

int frankenphp_reset_opcache(void) {
zend_function *opcache_reset =
Expand All @@ -1266,7 +1276,7 @@ static zend_module_entry **modules = NULL;
static int modules_len = 0;
static int (*original_php_register_internal_extensions_func)(void) = NULL;

PHPAPI int register_internal_extensions(void) {
int register_internal_extensions(void) {
if (original_php_register_internal_extensions_func != NULL &&
original_php_register_internal_extensions_func() != SUCCESS) {
return FAILURE;
Expand Down
26 changes: 1 addition & 25 deletions frankenphp.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,10 @@ package frankenphp

// #include <stdlib.h>
// #include <stdint.h>
// #include "frankenphp.h"
// #include <php_variables.h>
// #include <zend_llist.h>
// #include <SAPI.h>
// #include "frankenphp.h"
import "C"
import (
"bytes"
Expand Down Expand Up @@ -753,30 +753,6 @@ func go_is_context_done(threadIndex C.uintptr_t) C.bool {
return C.bool(phpThreads[threadIndex].frankenPHPContext().isDone)
}

// ExecuteScriptCLI executes the PHP script passed as parameter.
// It returns the exit status code of the script.
func ExecuteScriptCLI(script string, args []string) int {
// Ensure extensions are registered before CLI execution
registerExtensions()

cScript := C.CString(script)
defer C.free(unsafe.Pointer(cScript))

argc, argv := convertArgs(args)
defer freeArgs(argv)

return int(C.frankenphp_execute_script_cli(cScript, argc, (**C.char)(unsafe.Pointer(&argv[0])), false))
}

func ExecutePHPCode(phpCode string) int {
// Ensure extensions are registered before CLI execution
registerExtensions()

cCode := C.CString(phpCode)
defer C.free(unsafe.Pointer(cCode))
return int(C.frankenphp_execute_script_cli(cCode, 0, nil, true))
}

func convertArgs(args []string) (C.int, []*C.char) {
argc := C.int(len(args))
argv := make([]*C.char, argc)
Expand Down
42 changes: 42 additions & 0 deletions frankenphp.h
Original file line number Diff line number Diff line change
@@ -1,6 +1,46 @@
#ifndef _FRANKENPHP_H
#define _FRANKENPHP_H

#ifdef _WIN32
// Define this to prevent windows.h from including legacy winsock.h
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif

// Explicitly include Winsock2 BEFORE windows.h
#include <windows.h>
#include <winerror.h>
#include <winsock2.h>
#include <ws2tcpip.h>

// Fix for missing IntSafe functions (LongLongAdd) when building with Clang
#ifdef __clang__
#ifndef INTSAFE_E_ARITHMETIC_OVERFLOW
#define INTSAFE_E_ARITHMETIC_OVERFLOW ((HRESULT)0x80070216L)
#endif

#ifndef LongLongAdd
static inline HRESULT LongLongAdd(LONGLONG llAugend, LONGLONG llAddend,
LONGLONG *pllResult) {
if (__builtin_add_overflow(llAugend, llAddend, pllResult)) {
return INTSAFE_E_ARITHMETIC_OVERFLOW;
}
return S_OK;
}
#endif

#ifndef LongLongSub
static inline HRESULT LongLongSub(LONGLONG llMinuend, LONGLONG llSubtrahend,
LONGLONG *pllResult) {
if (__builtin_sub_overflow(llMinuend, llSubtrahend, pllResult)) {
return INTSAFE_E_ARITHMETIC_OVERFLOW;
}
return S_OK;
}
#endif
#endif
#endif

#include <Zend/zend_modules.h>
#include <Zend/zend_types.h>
#include <stdbool.h>
Expand Down Expand Up @@ -47,8 +87,10 @@ bool frankenphp_shutdown_dummy_request(void);
int frankenphp_execute_script(char *file_name);
void frankenphp_update_local_thread_context(bool is_worker);

#ifndef ZEND_WIN32
int frankenphp_execute_script_cli(char *script, int argc, char **argv,
bool eval);
#endif

void frankenphp_register_variables_from_request_info(
zval *track_vars_array, zend_string *content_type,
Expand Down
43 changes: 0 additions & 43 deletions frankenphp_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -733,40 +733,6 @@ func testFileUpload(t *testing.T, opts *testOptions) {
}, opts)
}

func TestExecuteScriptCLI(t *testing.T) {
if _, err := os.Stat("internal/testcli/testcli"); err != nil {
t.Skip("internal/testcli/testcli has not been compiled, run `cd internal/testcli/ && go build`")
}

cmd := exec.Command("internal/testcli/testcli", "testdata/command.php", "foo", "bar")
stdoutStderr, err := cmd.CombinedOutput()
assert.Error(t, err)

var exitError *exec.ExitError
if errors.As(err, &exitError) {
assert.Equal(t, 3, exitError.ExitCode())
}

stdoutStderrStr := string(stdoutStderr)

assert.Contains(t, stdoutStderrStr, `"foo"`)
assert.Contains(t, stdoutStderrStr, `"bar"`)
assert.Contains(t, stdoutStderrStr, "From the CLI")
}

func TestExecuteCLICode(t *testing.T) {
if _, err := os.Stat("internal/testcli/testcli"); err != nil {
t.Skip("internal/testcli/testcli has not been compiled, run `cd internal/testcli/ && go build`")
}

cmd := exec.Command("internal/testcli/testcli", "-r", "echo 'Hello World';")
stdoutStderr, err := cmd.CombinedOutput()
assert.NoError(t, err)

stdoutStderrStr := string(stdoutStderr)
assert.Equal(t, stdoutStderrStr, `Hello World`)
}

func ExampleServeHTTP() {
if err := frankenphp.Init(); err != nil {
panic(err)
Expand All @@ -786,15 +752,6 @@ func ExampleServeHTTP() {
log.Fatal(http.ListenAndServe(":8080", nil))
}

func ExampleExecuteScriptCLI() {
if len(os.Args) <= 1 {
log.Println("Usage: my-program script.php")
os.Exit(1)
}

os.Exit(frankenphp.ExecuteScriptCLI(os.Args[1], os.Args))
}

func BenchmarkHelloWorld(b *testing.B) {
require.NoError(b, frankenphp.Init())
b.Cleanup(frankenphp.Shutdown)
Expand Down
2 changes: 2 additions & 0 deletions internal/cpu/cpu_unix.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
//go:build unix

package cpu

// #include <time.h>
Expand Down
2 changes: 1 addition & 1 deletion internal/cpu/cpu_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import (
)

// ProbeCPUs fallback that always determines that the CPU limits are not reached
func ProbeCPUs(probeTime time.Duration, maxCPUUsage float64, abort chan struct{}) bool {
func ProbeCPUs(probeTime time.Duration, _ float64, abort chan struct{}) bool {
select {
case <-abort:
return false
Expand Down
Loading
Loading