Compare commits

...

88 Commits

Author SHA1 Message Date
dependabot[bot]
6e6f665d82 ci: bump the github-actions group with 3 updates
Bumps the github-actions group with 3 updates: [actions/upload-artifact](https://github.com/actions/upload-artifact), [actions/download-artifact](https://github.com/actions/download-artifact) and [actions/cache](https://github.com/actions/cache).


Updates `actions/upload-artifact` from 5 to 6
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v5...v6)

Updates `actions/download-artifact` from 6 to 7
- [Release notes](https://github.com/actions/download-artifact/releases)
- [Commits](https://github.com/actions/download-artifact/compare/v6...v7)

Updates `actions/cache` from 4 to 5
- [Release notes](https://github.com/actions/cache/releases)
- [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md)
- [Commits](https://github.com/actions/cache/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
- dependency-name: actions/download-artifact
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
- dependency-name: actions/cache
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-22 11:07:22 +00:00
Kévin Dunglas
57c58faf1c chore: prepare release 1.11.1 2025-12-20 09:16:23 +01:00
Loric Brevet
25d9cb9600 fix: crash when using the logger outside of the a request context 2025-12-20 09:15:29 +01:00
Kévin Dunglas
4092ecb5b5 fix: frankenphp_log() level parameter must be optional 2025-12-19 16:25:32 +01:00
Kévin Dunglas
75ccccf1b2 fix(caddy): use default patterns when hot_reload is alone 2025-12-19 09:38:05 +01:00
Kévin Dunglas
6231bf4a1c chore: prepare release 1.11.0 2025-12-18 16:51:41 +01:00
Kévin Dunglas
e01e40fd97 chore: bump deps (#2078) 2025-12-17 11:47:14 +01:00
Alexander Stecher
175e644d10 feat: multiple curly braces for watcher (#2068)
Allows doing something like this:

```caddyfile
watch "/app/{config,src}/*.{php,js}"
```

In the long term it would be nice to have pattern matching in the
watcher repo itself
2025-12-17 00:22:28 +01:00
Kévin Dunglas
a8f75d0eef ci: verbose logs for StaticPHP (#2074) 2025-12-15 20:13:15 +01:00
Raphael Coeffic
91c553f3d9 feat: add support for structured logging with the frankenphp_log() PHP function (#1979)
As discussed in https://github.com/php/frankenphp/discussions/1961,
there is no real way to pass a severity/level to any log handler offered
by PHP that would make it to the FrankenPHP layer. This new function
allows applications embedding FrankenPHP to integrate PHP logging into
the application itself, thus offering a more streamlined experience.

---------

Co-authored-by: Quentin Burgess <qutn.burgess@gmail.com>
Co-authored-by: Kévin Dunglas <kevin@dunglas.fr>
2025-12-15 16:10:35 +01:00
Alexandre Daubois
7fca07ed67 feat(types): expose IsPacked to help dealing with hashmaps and lists in Go code 2025-12-15 15:35:21 +01:00
dependabot[bot]
3599299cde chore(caddy): bump github.com/spf13/cobra
Bumps the go-modules group in /caddy with 1 update: [github.com/spf13/cobra](https://github.com/spf13/cobra).


Updates `github.com/spf13/cobra` from 1.10.1 to 1.10.2
- [Release notes](https://github.com/spf13/cobra/releases)
- [Commits](https://github.com/spf13/cobra/compare/v1.10.1...v1.10.2)

---
updated-dependencies:
- dependency-name: github.com/spf13/cobra
  dependency-version: 1.10.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: go-modules
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-15 13:41:56 +01:00
Alexandre Daubois
bb1c3678dc feat(extgen): add support for callable in parameters (#1731) 2025-12-15 12:50:50 +01:00
dependabot[bot]
58a63703b4 ci: bump actions/checkout from 5 to 6 in the github-actions group
Bumps the github-actions group with 1 update: [actions/checkout](https://github.com/actions/checkout).


Updates `actions/checkout` from 5 to 6
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-15 12:23:53 +01:00
Alexandre Daubois
694ab86cef doc(runtime): mention Symfony 7.4 native support for worker mode (#1668)
Fixes https://github.com/symfony/symfony-docs/issues/21099, related to
https://github.com/symfony/symfony/pull/60503
2025-12-14 17:06:28 +01:00
Marc
e23e0c571e update config doc for new deb/rpm packages (#2071) 2025-12-13 19:32:27 +01:00
Kévin Dunglas
f02e6f2f85 fix: update mercure_publish() to use the new GoPackedArray() API 2025-12-13 19:30:22 +01:00
Alexander Stecher
11213fd1de fix: returns a zend_array directly in types.go (#1894) 2025-12-12 22:55:58 +01:00
Alexandre Daubois
41da660088 fix(hot-reload): fix import (#2069) 2025-12-12 15:23:55 +01:00
Alexandre Daubois
599c92b15d tests(extgen): add integration tests (#1984)
Fix #1975
2025-12-12 14:32:00 +01:00
Kévin Dunglas
225ca409d3 feat: hot reload (#2031)
This patch brings hot reloading capabilities to PHP apps: in
development, the browser will automatically refresh the page when any
source file changes!
It's similar to HMR in JavaScript.

It is built on top of [the watcher
mechanism](https://frankenphp.dev/docs/config/#watching-for-file-changes)
and of the [Mercure](https://frankenphp.dev/docs/mercure/) integration.

Each time a watched file is modified, a Mercure update is sent, giving
the ability to the client to reload the page, or part of the page
(assets, images...).

Here is an example implementation:

```caddyfile
root ./public


mercure {
      subscriber_jwt {env.MERCURE_SUBSCRIBER_JWT_KEY}
      anonymous
}

php_server {
      hot_reload
}
```

```php
<?php
header('Content-Type: text/html');
?>
<!DOCTYPE html>
<html lang="en">
<head>
<title>Test</title>
<script>
    const es = new EventSource('<?=$_SERVER['FRANKENPHP_HOT_RELOAD']?>');
    es.onmessage = () => location.reload();
</script>
</head>
<body>
Hello
```

I plan to create a helper JS library to handle more advanced cases
(reloading CSS, JS, etc), similar to [HotWire
Spark](https://github.com/hotwired/spark). Be sure to attend my
SymfonyCon to learn more!

There is still room for improvement:

- Provide an option to only trigger the update without reloading the
worker for some files (ex, images, JS, CSS...)
- Support classic mode (currently, only the worker mode is supported)
- Don't reload all workers when only the files used by one change

However, this PR is working as-is and can be merged as a first step.

This patch heavily refactors the watcher module. Maybe it will be
possible to extract it as a standalone library at some point (would be
useful to add a similar feature but not tight to PHP as a Caddy module).

---------

Signed-off-by: Kévin Dunglas <kevin@dunglas.fr>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-12-12 14:29:18 +01:00
Francis Lavoie
d2007620a4 docs: Fix file extension in FrankenPHP configuration (#2067)
Corrected the file extension for additional configuration files in
FrankenPHP section.

---------

Signed-off-by: Francis Lavoie <lavofr@gmail.com>
2025-12-11 21:40:34 +01:00
Kévin Dunglas
4ac024a1d0 fix: remove deprecated Mercure "transport_url" directive from Caddyfile 2025-12-10 15:40:21 +01:00
Kacper Rowiński
e0dcf42852 chore: bump github.com/smallstep/certificates/ from 0.28.4 to 0.29.0 2025-12-09 11:40:58 +01:00
Arkeins
7b8cf6b127 docs: wrong config path for autoloaded .caddyfile (#2059)
Hi !

I am playing with FrankenPHP, and frankly new to the thing so I may be
wrong.

The [current documentation](https://frankenphp.dev/docs/config/#docker)
mention that :

> /etc/frankenphp/caddy.d/*.caddy: additional configuration files that
are loaded automatically

But in the main Caddyfile ([here on
GitHub](https://github.com/php/frankenphp/blob/main/caddy/frankenphp/Caddyfile#L59))
imports differently:

```ini
import Caddyfile.d/*.caddyfile
```

This PR aims to correct this :)

Thank you for your time !

Signed-off-by: Arkeins <7311955+Arkeins@users.noreply.github.com>
2025-12-03 18:20:09 +01:00
Alexander Stecher
98573ed7c0 refactor: extract the state module and make the backoff error instead of panic
This PR:
- moves state.go to its own module
- moves the phpheaders test the phpheaders module
- simplifies backoff.go
- makes the backoff error instead of panic (so it can be tested)
- removes some unused C structs
2025-12-02 23:10:12 +01:00
Kévin Dunglas
16e2bbb969 tests: improve benchmarks 2025-12-02 17:13:38 +01:00
Kévin Dunglas
816bcc2ad6 chore: make super-linter green (#2051) 2025-12-01 15:52:08 +01:00
Alexandre Daubois
1fbabf91c9 fix(extgen): use RETURN_EMPTY_STRING() when returning empty string (#2049)
One last bug spotted by #1984, empty strings should be returned using
`RETURN_EMPTY_STRING()` or it may segfault.
2025-12-01 15:43:45 +01:00
Alexandre Daubois
2fa7663d3b fix(extgen): use REGISTER(_NS)_BOOL_CONSTANT (#2047)
Spotted in #1984, this is the right macros to declare boolean constants
2025-12-01 15:35:34 +01:00
Kévin Dunglas
b1bdce359b tests: simplify benchmarks code 2025-12-01 15:34:04 +01:00
Max
c9ad9fc55a headerKeyCache: use otter v2 (#2040)
Benchmarks show that version 1, while extremely fast with hot keys,
becomes several times slower than no‑cache under frequent misses.
Version 2 delivers consistently better performance across all scenarios,
with no allocations and stable latency.

```
BenchmarkGetUnCommonHeaderNoCacheSequential-12                           7545640               169.4 ns/op            72 B/op          4 allocs/op
BenchmarkGetUnCommonHeaderV2Sequential-12                               14471982                85.98 ns/op            0 B/op          0 allocs/op
BenchmarkGetUnCommonHeaderV1Sequential-12                               19748048                59.63 ns/op            0 B/op          0 allocs/op

BenchmarkGetUnCommonHeaderNoCacheParallelOneKey-12                      24352088                44.47 ns/op           72 B/op          4 allocs/op
BenchmarkGetUnCommonHeaderV2ParallelOneKey-12                           91024160                11.76 ns/op            0 B/op          0 allocs/op
BenchmarkGetUnCommonHeaderV1ParallelOneKey-12                           192048842                6.186 ns/op           0 B/op          0 allocs/op

BenchmarkGetUnCommonHeaderNoCacheParallelRandomMaximumSize-12           26261611                43.07 ns/op           62 B/op          3 allocs/op
BenchmarkGetUnCommonHeaderV2ParallelRandomMaximumSize-12                100000000               14.43 ns/op            0 B/op          0 allocs/op
BenchmarkGetUnCommonHeaderV1ParallelRandomMaximumSize-12                137813384                8.965 ns/op           0 B/op          0 allocs/op

BenchmarkGetUnCommonHeaderNoCacheParallelRandomLenKeys-12               24224664                46.57 ns/op           71 B/op          3 allocs/op
BenchmarkGetUnCommonHeaderV2ParallelRandomLenKeys-12                    69002575                17.42 ns/op            0 B/op          0 allocs/op
BenchmarkGetUnCommonHeaderV1ParallelRandomLenKeys-12                     8498404               253.1 ns/op            42 B/op          1 allocs/op
```

---------

Co-authored-by: Kévin Dunglas <kevin@dunglas.fr>
2025-12-01 11:37:40 +01:00
Marc
12d4c3d09b [docs] update other languages based on English (#2044)
They were a little out of date, making use of AI to update them to the
same format.
2025-12-01 09:08:53 +01:00
Marc
7fceb32f7b give /var/lib/frankenphp sys_rw_content_t permissions for mercure.db files (#2037)
The current configuration is not able to start FrankenPHP when mercure
and SELinux are used with a Caddyfile like this:

```Caddyfile
mercure {
    transport bolt {
        path mercure.db
    }
}
```

closes https://github.com/php/frankenphp/issues/2035

Exact error:
```
SELinux is preventing /usr/bin/frankenphp from map access on the file /var/lib/frankenphp/mercure.db.

*****  Plugin catchall_boolean (89.3 confidence) suggests   ******************

If you want to allow domain to can mmap files
Then you must tell SELinux about this by enabling the 'domain_can_mmap_files' boolean.

Do
setsebool -P domain_can_mmap_files 1

*****  Plugin catchall (11.6 confidence) suggests   **************************

If you believe that frankenphp should be allowed map access on the mercure.db file by default.
Then you should report this as a bug.
You can generate a local policy module to allow this access.
Do
allow this access for now by executing:
# ausearch -c 'frankenphp' --raw | audit2allow -M my-frankenphp
# semodule -X 300 -i my-frankenphp.pp


Additional Information:
Source Context                system_u:system_r:httpd_t:s0
Target Context                system_u:object_r:httpd_var_lib_t:s0
Target Objects                /var/lib/frankenphp/mercure.db [ file ]
Source                        frankenphp
Source Path                   /usr/bin/frankenphp
Port                          <Unknown>
Host                          localhost
Source RPM Packages           frankenphp-1.10.0_84-1.x86_64
Target RPM Packages
SELinux Policy RPM            selinux-policy-targeted-3.14.3-139.el8_10.1.noarch
Local Policy RPM              selinux-policy-targeted-3.14.3-139.el8_10.1.noarch
Selinux Enabled               True
Policy Type                   targeted
Enforcing Mode                Enforcing
Host Name                     localhost
Platform                      Linux localhost
                              4.18.0-553.81.1.el8_10.x86_64 #1 SMP Mon Oct 27
                              11:29:19 EDT 2025 x86_64 x86_64
Alert Count                   12
First Seen                    2025-10-29 17:25:26 CET
Last Seen                     2025-11-25 17:18:19 CET
Local ID                      c4e79504-117e-4e9f-ad8c-f0bcc4856697

Raw Audit Messages
type=AVC msg=audit(1764087499.320:475517): avc:  denied  { map } for  pid=322613 comm="frankenphp" path="/var/lib/frankenphp/mercure.db" dev="md3" ino=93716492 scontext=system_u:system_r:httpd_t:s0 tcontext=system_u:object_r:httpd_var_lib_t:s0 tclass=file permissive=0


type=SYSCALL msg=audit(1764087499.320:475517): arch=x86_64 syscall=mmap success=no exit=EACCES a0=0 a1=8000 a2=1 a3=1 items=0 ppid=1 pid=322613 auid=4294967295 uid=991 gid=988 euid=991 suid=991 fsuid=991 egid=988 sgid=988 fsgid=988 tty=(none) ses=4294967295 comm=frankenphp exe=/usr/bin/frankenphp subj=system_u:system_r:httpd_t:s0 key=(null)

Hash: frankenphp,httpd_t,httpd_var_lib_t,file,map
```
2025-11-28 11:11:28 +01:00
Marc
1b30905c26 fox(static): add watcher to defaultExtensionLibs (#2039)
fix https://github.com/php/frankenphp/issues/2038
2025-11-27 00:10:41 +01:00
Alexander Stecher
dadeb5a628 perf: tail latency with goSched (#2033)
Alternate implementation to #2016 that doesn't reduce RPS with lower
amounts of threads
2025-11-26 18:33:07 +01:00
Marc
abaf03c7f7 deduplicate installation instructions in README(#2013) 2025-11-26 08:19:27 +01:00
Kévin Dunglas
fc5f6ef092 chore: prepare release 1.10.1 2025-11-25 10:54:34 +01:00
Marc
65111334a1 docs: update issue template to differentiate between deb and RPM packages 2025-11-25 10:50:38 +01:00
dependabot[bot]
6747aaae2d ci: bump actions/checkout from 5 to 6 in the github-actions group
Bumps the github-actions group with 1 update: [actions/checkout](https://github.com/actions/checkout).


Updates `actions/checkout` from 5 to 6
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-25 10:49:11 +01:00
Kévin Dunglas
6c764ad9c5 fix: correctly set the Mercure hub for the main worker request 2025-11-24 11:21:45 +01:00
Kévin Dunglas
e6b3f70d91 chore: bump deps 2025-11-23 23:13:12 +01:00
Kévin Dunglas
911e6d156b fix: crash when a string is passed for the topics parameter of the mercure_publish() function (#2021) 2025-11-23 17:03:43 +01:00
Kévin Dunglas
c6cadf3bf6 chore: prepare release 1.10.0 2025-11-21 16:16:01 +01:00
Marc
f28f6e8d03 docs: update docs for rpm packages and extension availability (#1988)
continues https://github.com/php/frankenphp/pull/1756

---------

Co-authored-by: Kévin Dunglas <kevin@dunglas.fr>
2025-11-21 14:31:43 +01:00
Antonin CLAUZIER
01beb66573 ci: PHP 8.5 (#2006)
Co-authored-by: Marc <m@pyc.ac>
Co-authored-by: Kévin Dunglas <kevin@dunglas.fr>
2025-11-21 13:54:07 +01:00
Kévin Dunglas
41e0713a1b fix: allow null for mercure_publish() retry parameter 2025-11-21 13:52:55 +01:00
Kévin Dunglas
bbfb1b0a0e ci: upgrade macOS runners 2025-11-21 13:51:15 +01:00
Marc
49e98cc8d6 delete source/downloads after building in script, add .editorconfig (#2000)
* delete source/downloads after building in script, not in dockerfile

* add editorconfig

* eol

* cs fix

* added \n there

* we expect Hello\n

* Change tab width for shell scripts to 4 spaces

* bring back embed comment
2025-11-20 11:49:09 +01:00
Kévin Dunglas
c93729e136 chore: use sync.WaitGroup.Go when possible (#1996)
* chore: use sync.WaitGroup.Go when possible

* Update internal/watcher/watcher.go

Co-authored-by: Alexandre Daubois <2144837+alexandre-daubois@users.noreply.github.com>

---------

Co-authored-by: Alexandre Daubois <2144837+alexandre-daubois@users.noreply.github.com>
2025-11-20 11:48:18 +01:00
Kévin Dunglas
ea042637e6 ci: add back ARMv6 build 2025-11-20 11:47:36 +01:00
Marc
0b74507945 don't upx pack on macos (#2003) 2025-11-20 10:19:27 +01:00
Kévin Dunglas
aa1bd23004 ci: use local sources when building with Bake 2025-11-20 05:48:40 +01:00
Marc
02f900bb97 we use "mac" for os-name, not "darwin" (#2001) 2025-11-19 22:11:23 +01:00
Vincent Amstoutz
56df2666e1 ci: add PHP 8.5 support for building images 2025-11-19 16:19:32 +01:00
Marc
1de9073e49 simplify build-static script (#1968)
* simplify build-static script

* we don't require go anymore, since spc will install it

* bring back eof newline

* move to frankenphp-os-arch again

* shell fmt

* Add FrankenPHP Caddy modules to build script
2025-11-19 15:30:00 +01:00
Kévin Dunglas
36062a0dce feat(static): add XSL extension (#1998) 2025-11-19 14:55:53 +01:00
Kévin Dunglas
10cf2c4a2e fix: use the global logger during classes preloading (#1994)
* fix: use the global logger during classes preloading

* better fix

* fix comparision

* Update frankenphp.go
2025-11-19 14:18:29 +01:00
Ahmet Türk
f224f8e391 docs: fix minor typo (#1991) 2025-11-18 14:29:25 +01:00
Alexander Stecher
0b2d3c913f feat: per worker max threads (#1962)
* adds worker max_threads

* Adds tests for all calculation cases.

* Adds max_threads limitation to test.

* Removes the test sleep.

* Adds max_threads to error message.

* correctly uses continue.

* Fixes logic with only worker max_threads set.

* Adjust comments.

* Removes unnecessary check.

* Fixes comment.

* suggestions by @dunlgas.

* copilot suggestions.

* Renames logger.

* review

---------

Co-authored-by: Kévin Dunglas <kevin@dunglas.fr>
2025-11-18 11:55:29 +01:00
Kévin Dunglas
75a48e81a7 chore: bump deps 2025-11-18 11:48:59 +01:00
Alexandre Daubois
bd943f49de feat(extgen): print gen_stub.php in case of failure 2025-11-18 11:10:03 +01:00
Alexandre Daubois
8f298ab060 fix(extgen): constant should be declared under the namespace provided by export_php:namespace 2025-11-18 10:40:59 +01:00
Kévin Dunglas
41cb2bbeaa feat: mercure_publish() PHP function to dispatch Mercure updates (#1927)
* feat: mercure_publish() PHP function to dispatch Mercure updates

* fix stubs for old versions

* review

* cleanup and fixes
2025-11-18 09:59:53 +01:00
Marc
853cb67e95 shallow clone to save space in CI (#1987)
* shallow clone

* also remove source dir after building in CI

* formatting

* pass them through?

* only CI

* add as variable
2025-11-18 08:48:06 +01:00
Alexandre Daubois
eeb7d1a0c4 fix(extgen): only register ext_functions if functions are declared 2025-11-17 17:40:30 +01:00
Kévin Dunglas
8341cc98c6 refactor: rely on context.Context for log/slog and others (#1969)
* refactor: rely on context.Context for log/slog and others

* optimize

* refactor

* Apply suggestion from @Copilot

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* fix watcher-skip

* better globals handling

* fix

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-11-17 16:32:23 +01:00
Alexandre Daubois
40cb42aace chore: bump net 2025-11-17 15:09:30 +01:00
Alexandre Daubois
1e48fbb801 chore(docs): catch-up french translation (#1978) 2025-11-17 12:07:38 +01:00
Alexandre Daubois
4e6d67e0b4 fix(extgen): don't remove everything in the build directory now that there's no build subdir 2025-11-14 15:13:54 +01:00
Alexandre Daubois
18946308fd docs: remove superfluous arg in an example (#1972) 2025-11-14 15:12:56 +01:00
Alexandre Daubois
f7298557aa feat(extgen): automatically add "runtime/cgo" to the imports if necessary 2025-11-14 15:12:28 +01:00
Alexandre Daubois
861b345b05 fix(extgen): replace any by interface{} in the generated go file when dealing with handles 2025-11-14 14:54:40 +01:00
Kévin Dunglas
724c0b11ca feat: set a custom Server header
# Conflicts:
#	caddy/module.go
#	frankenphp.go
2025-11-10 17:25:22 +01:00
dependabot[bot]
63168e087e ci: bump golangci/golangci-lint-action in the github-actions group
Bumps the github-actions group with 1 update: [golangci/golangci-lint-action](https://github.com/golangci/golangci-lint-action).


Updates `golangci/golangci-lint-action` from 8 to 9
- [Release notes](https://github.com/golangci/golangci-lint-action/releases)
- [Commits](https://github.com/golangci/golangci-lint-action/compare/v8...v9)

---
updated-dependencies:
- dependency-name: golangci/golangci-lint-action
  dependency-version: '9'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-10 17:24:24 +01:00
Kévin Dunglas
6225da9c18 refactor: improve ExtensionWorkers API (#1952)
* refactor: improve ExtensionWorkers API

* Update workerextension.go

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update workerextension.go

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update caddy/app.go

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Apply suggestion from @Copilot

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* review

* fix tests

* docs

* errors

* improved error handling

* fix race

* add missing return

* use %q in Errorf

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-11-10 14:12:14 +01:00
Alexander Stecher
407ef09ac3 fix: fail immediately on missing worker file (#1963) 2025-11-10 09:23:50 +01:00
Marc
bf4c9fe986 fix test typo (#1964)
* Fix typo in TestFlushEmptyResponse_worker function

* Fix test function name for flush empty response
2025-11-08 08:28:56 +01:00
Marc
b22bdd987b update docs to remove old references to frankenphp:static-builder (#1950)
* update docs to remove old references to frankenphp:static-builder (we have -musl and -gnu)

* remove --platform and make gnu default

* add --platform back in (let dunglas decide)

* fix missed translations
2025-11-07 10:27:37 +01:00
Alexandre Daubois
28d17b39dc chore: bump GitHub Action deps (#1957) 2025-11-04 08:52:23 +01:00
Marc
264f92835d bring back logic for workers to inherit php_server parent environment (#1956)
* bring back logic to inherit php_server parent environment

* change order to account for

php {
    worker file.php 1 {

    }
}
cases

* suggestion

* add inherit env test
2025-11-02 14:50:50 +01:00
Alexandre Daubois
b49aed1934 chore: bump deps 2025-10-31 16:51:26 +01:00
Kévin Dunglas
4d0fb7d0f8 refactor: simplify Init() 2025-10-29 23:14:46 +01:00
Marc
5447a7a6c8 add compile from sources fallback to unsupported OS message (#1939)
* add compile from sources fallback to unsupported OS message

* rewrite message to indicate general support, but no precompiled binaries

* add logs to build-static
2025-10-29 19:25:51 +01:00
Alexander Stecher
1270784cd3 suggestion: external worker api (#1928)
* Cleaner request apis.
2025-10-29 11:36:33 +01:00
Kévin Dunglas
9b8d215727 refactor: improve Worker public API and docs 2025-10-29 11:36:33 +01:00
Alexandre Daubois
94e58eb215 fix: replace file_put_contents() by file_get_contents() in Mercure docs 2025-10-28 15:00:43 +01:00
Alexander Stecher
bf6e6534f6 fix: exit() and dd() support in worker mode (#1946)
* Verifies exit behavior.

* formatting

* Checks for actual exit.

* Fixes test.

* Fixes test.

* Update testdata/dd.php

Co-authored-by: Kévin Dunglas <kevin@dunglas.fr>

---------

Co-authored-by: Kévin Dunglas <kevin@dunglas.fr>
2025-10-28 10:57:50 +01:00
169 changed files with 7163 additions and 3110 deletions

1
.clang-format-ignore Normal file
View File

@@ -0,0 +1 @@
frankenphp_arginfo.h

13
.editorconfig Normal file
View File

@@ -0,0 +1,13 @@
root = true
[*]
end_of_line = lf
insert_final_newline = true
[*.sh]
indent_style = tab
tab_width = 4
[*.Dockerfile]
indent_style = tab
tab_width = 4

View File

@@ -29,8 +29,9 @@ body:
- Docker (Debian Trixie)
- Docker (Debian Bookworm)
- Docker (Alpine)
- Official static build
- Standalone binary
- deb packages
- rpm packages
- Static binary
- Custom (tell us more in the description)
default: 0
validations:

View File

@@ -11,6 +11,8 @@ updates:
go-modules:
patterns:
- "*"
cooldown:
default-days: 7
- package-ecosystem: gomod
directory: /caddy
schedule:
@@ -21,6 +23,8 @@ updates:
go-modules:
patterns:
- "*"
cooldown:
default-days: 7
- package-ecosystem: github-actions
directory: /
schedule:
@@ -31,3 +35,5 @@ updates:
github-actions:
patterns:
- "*"
cooldown:
default-days: 7

View File

@@ -46,6 +46,8 @@ jobs:
php_version: ${{ steps.check.outputs.php_version }}
php82_version: ${{ steps.check.outputs.php82_version }}
php83_version: ${{ steps.check.outputs.php83_version }}
php84_version: ${{ steps.check.outputs.php84_version }}
php85_version: ${{ steps.check.outputs.php85_version }}
skip: ${{ steps.check.outputs.skip }}
ref: ${{ steps.check.outputs.ref || (github.event_name == 'workflow_dispatch' && inputs.version) || '' }}
steps:
@@ -57,11 +59,13 @@ jobs:
PHP_82_LATEST=$(skopeo inspect docker://docker.io/library/php:8.2 --override-os linux --override-arch amd64 | jq -r '.Env[] | select(test("^PHP_VERSION=")) | sub("^PHP_VERSION="; "")')
PHP_83_LATEST=$(skopeo inspect docker://docker.io/library/php:8.3 --override-os linux --override-arch amd64 | jq -r '.Env[] | select(test("^PHP_VERSION=")) | sub("^PHP_VERSION="; "")')
PHP_84_LATEST=$(skopeo inspect docker://docker.io/library/php:8.4 --override-os linux --override-arch amd64 | jq -r '.Env[] | select(test("^PHP_VERSION=")) | sub("^PHP_VERSION="; "")')
PHP_85_LATEST=$(skopeo inspect docker://docker.io/library/php:8.5 --override-os linux --override-arch amd64 | jq -r '.Env[] | select(test("^PHP_VERSION=")) | sub("^PHP_VERSION="; "")')
{
echo php_version="${PHP_82_LATEST},${PHP_83_LATEST},${PHP_84_LATEST}"
echo php_version="${PHP_82_LATEST},${PHP_83_LATEST},${PHP_84_LATEST},${PHP_85_LATEST}"
echo php82_version="${PHP_82_LATEST//./-}"
echo php83_version="${PHP_83_LATEST//./-}"
echo php84_version="${PHP_84_LATEST//./-}"
echo php85_version="${PHP_85_LATEST//./-}"
} >> "${GITHUB_OUTPUT}"
# Check if the Docker images must be rebuilt
@@ -75,8 +79,9 @@ jobs:
FRANKENPHP_82_LATEST=$(skopeo inspect docker://docker.io/dunglas/frankenphp:"${FRANKENPHP_LATEST_TAG_NO_PREFIX}"-php8.2 --override-os linux --override-arch amd64 | jq -r '.Env[] | select(test("^PHP_VERSION=")) | sub("^PHP_VERSION="; "")')
FRANKENPHP_83_LATEST=$(skopeo inspect docker://docker.io/dunglas/frankenphp:"${FRANKENPHP_LATEST_TAG_NO_PREFIX}"-php8.3 --override-os linux --override-arch amd64 | jq -r '.Env[] | select(test("^PHP_VERSION=")) | sub("^PHP_VERSION="; "")')
FRANKENPHP_84_LATEST=$(skopeo inspect docker://docker.io/dunglas/frankenphp:"${FRANKENPHP_LATEST_TAG_NO_PREFIX}"-php8.4 --override-os linux --override-arch amd64 | jq -r '.Env[] | select(test("^PHP_VERSION=")) | sub("^PHP_VERSION="; "")')
FRANKENPHP_85_LATEST=$(skopeo inspect docker://docker.io/dunglas/frankenphp:"${FRANKENPHP_LATEST_TAG_NO_PREFIX}"-php8.5 --override-os linux --override-arch amd64 | jq -r '.Env[] | select(test("^PHP_VERSION=")) | sub("^PHP_VERSION="; "")')
if [[ "${FRANKENPHP_82_LATEST}" == "${PHP_82_LATEST}" ]] && [[ "${FRANKENPHP_83_LATEST}" == "${PHP_83_LATEST}" ]] && [[ "${FRANKENPHP_84_LATEST}" == "${PHP_84_LATEST}" ]]; then
if [[ "${FRANKENPHP_82_LATEST}" == "${PHP_82_LATEST}" ]] && [[ "${FRANKENPHP_83_LATEST}" == "${PHP_83_LATEST}" ]] && [[ "${FRANKENPHP_84_LATEST}" == "${PHP_84_LATEST}" ]] && [[ "${FRANKENPHP_85_LATEST}" == "${PHP_85_LATEST}" ]]; then
echo skip=true >> "${GITHUB_OUTPUT}"
exit 0
fi
@@ -85,7 +90,7 @@ jobs:
echo ref="${FRANKENPHP_LATEST_TAG}"
echo skip=false
} >> "${GITHUB_OUTPUT}"
- uses: actions/checkout@v5
- uses: actions/checkout@v6
if: ${{ !fromJson(steps.check.outputs.skip) }}
with:
ref: ${{ steps.check.outputs.ref }}
@@ -129,17 +134,25 @@ jobs:
platform: linux/arm/v6
- variant: php-${{ needs.prepare.outputs.php83_version }}-trixie
platform: linux/arm/v6
- variant: php-${{ needs.prepare.outputs.php84_version }}-trixie
platform: linux/arm/v6
- variant: php-${{ needs.prepare.outputs.php85_version }}-trixie
platform: linux/arm/v6
- variant: php-${{ needs.prepare.outputs.php82_version }}-bookworm
platform: linux/arm/v6
- variant: php-${{ needs.prepare.outputs.php83_version }}-bookworm
platform: linux/arm/v6
- variant: php-${{ needs.prepare.outputs.php84_version }}-bookworm
platform: linux/arm/v6
- variant: php-${{ needs.prepare.outputs.php85_version }}-bookworm
platform: linux/arm/v6
steps:
- name: Prepare
id: prepare
run: echo "sanitized_platform=${PLATFORM//\//-}" >> "${GITHUB_OUTPUT}"
env:
PLATFORM: ${{ matrix.platform }}
- uses: actions/checkout@v5
- uses: actions/checkout@v6
with:
ref: ${{ needs.prepare.outputs.ref }}
persist-credentials: false
@@ -159,6 +172,7 @@ jobs:
with:
pull: true
load: ${{ !fromJson(needs.prepare.outputs.push) }}
source: .
targets: |
builder-${{ matrix.variant }}
runner-${{ matrix.variant }}
@@ -194,7 +208,7 @@ jobs:
VARIANT: ${{ matrix.variant }}
- name: Upload builder metadata
if: fromJson(needs.prepare.outputs.push)
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v6
with:
name: metadata-builder-${{ matrix.variant }}-${{ steps.prepare.outputs.sanitized_platform }}
path: /tmp/metadata/builder/*
@@ -202,7 +216,7 @@ jobs:
retention-days: 1
- name: Upload runner metadata
if: fromJson(needs.prepare.outputs.push)
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v6
with:
name: metadata-runner-${{ matrix.variant }}-${{ steps.prepare.outputs.sanitized_platform }}
path: /tmp/metadata/runner/*
@@ -234,7 +248,7 @@ jobs:
target: ["builder", "runner"]
steps:
- name: Download metadata
uses: actions/download-artifact@v5
uses: actions/download-artifact@v7
with:
pattern: metadata-${{ matrix.target }}-${{ matrix.variant }}-*
path: /tmp/metadata

View File

@@ -20,7 +20,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout Code
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
fetch-depth: 0
persist-credentials: false
@@ -47,3 +47,5 @@ jobs:
VALIDATE_BIOME_LINT: false
# Conflicts with MARKDOWN
VALIDATE_MARKDOWN_PRETTIER: false
# To re-enable when https://github.com/super-linter/super-linter/issues/7244 will be closed
VALIDATE_EDITORCONFIG: false

View File

@@ -40,7 +40,7 @@ jobs:
steps:
- name: Remove local PHP
run: sudo apt-get remove --purge --autoremove 'php*' 'libmemcached*'
- uses: actions/checkout@v5
- uses: actions/checkout@v6
with:
persist-credentials: false
- uses: actions/setup-go@v6
@@ -52,12 +52,12 @@ jobs:
- name: Determine PHP version
id: determine-php-version
run: |
curl -fsSL 'https://www.php.net/releases/index.php?json&max=1&version=8.4' -o version.json
curl -fsSL 'https://www.php.net/releases/index.php?json&max=1&version=8.5' -o version.json
echo version="$(jq -r 'keys[0]' version.json)" >> "$GITHUB_OUTPUT"
echo archive="$(jq -r '.[] .source[] | select(.filename |endswith(".xz")) | "https://www.php.net/distributions/" + .filename' version.json)" >> "$GITHUB_OUTPUT"
- name: Cache PHP
id: cache-php
uses: actions/cache@v4
uses: actions/cache@v5
with:
path: php/target
key: php-sanitizers-${{ matrix.sanitizer }}-${{ runner.arch }}-${{ steps.determine-php-version.outputs.version }}

View File

@@ -10,7 +10,7 @@ on:
- main
paths:
- "docker-bake.hcl"
- ".github/workflows/docker.yaml"
- ".github/workflows/static.yaml"
- "**cgo.go"
- "**Dockerfile"
- "**.c"
@@ -37,6 +37,7 @@ permissions:
env:
IMAGE_NAME: ${{ (github.event_name == 'schedule' || (github.event_name == 'workflow_dispatch' && inputs.version) || startsWith(github.ref, 'refs/tags/')) && 'dunglas/frankenphp' || 'dunglas/frankenphp-dev' }}
SPC_OPT_BUILD_ARGS: --debug
GOTOOLCHAIN: local
jobs:
@@ -62,7 +63,7 @@ jobs:
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
REF: ${{ (github.ref_type == 'tag' && github.ref_name) || (github.event_name == 'workflow_dispatch' && inputs.version) || '' }}
- uses: actions/checkout@v5
- uses: actions/checkout@v6
with:
ref: ${{ steps.check.outputs.ref }}
persist-credentials: false
@@ -108,7 +109,7 @@ jobs:
run: echo "sanitized_platform=${PLATFORM//\//-}" >> "${GITHUB_OUTPUT}"
env:
PLATFORM: ${{ matrix.platform }}
- uses: actions/checkout@v5
- uses: actions/checkout@v6
with:
ref: ${{ needs.prepare.outputs.ref }}
persist-credentials: false
@@ -122,12 +123,26 @@ jobs:
with:
username: ${{ secrets.REGISTRY_USERNAME }}
password: ${{ secrets.REGISTRY_PASSWORD }}
- name: Set VERSION
run: |
if [ "${GITHUB_REF_TYPE}" == "tag" ]; then
export VERSION=${GITHUB_REF_NAME:1}
elif [ "${GITHUB_EVENT_NAME}" == "schedule" ]; then
export VERSION="${REF}"
else
export VERSION=${GITHUB_SHA}
fi
echo "VERSION=${VERSION}" >> "${GITHUB_ENV}"
env:
REF: ${{ needs.prepare.outputs.ref }}
- name: Build
id: build
uses: docker/bake-action@v6
with:
pull: true
load: ${{ !fromJson(needs.prepare.outputs.push) || matrix.debug || matrix.mimalloc }}
source: .
targets: static-builder-musl
set: |
${{ matrix.debug && 'static-builder-musl.args.DEBUG_SYMBOLS=1' || '' }}
@@ -141,7 +156,6 @@ jobs:
${{ (fromJson(needs.prepare.outputs.push) && !matrix.debug && !matrix.mimalloc) && format('*.output=type=image,name={0},push-by-digest=true,name-canonical=true,push=true', env.IMAGE_NAME) || '' }}
env:
SHA: ${{ github.sha }}
VERSION: ${{ (github.ref_type == 'tag' && github.ref_name) || needs.prepare.outputs.ref || 'dev' }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- # Workaround for https://github.com/actions/runner/pull/2477#issuecomment-1501003600
name: Export metadata
@@ -156,7 +170,7 @@ jobs:
METADATA: ${{ steps.build.outputs.metadata }}
- name: Upload metadata
if: fromJson(needs.prepare.outputs.push) && !matrix.debug && !matrix.mimalloc
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v6
with:
name: metadata-static-builder-musl-${{ steps.prepare.outputs.sanitized_platform }}
path: /tmp/metadata/*
@@ -174,7 +188,7 @@ jobs:
PLATFORM: ${{ matrix.platform }}
- name: Upload artifact
if: ${{ !fromJson(needs.prepare.outputs.push) }}
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v6
with:
name: frankenphp-linux-${{ matrix.platform == 'linux/amd64' && 'x86_64' || 'aarch64' }}${{ matrix.debug && '-debug' || '' }}${{ matrix.mimalloc && '-mimalloc' || '' }}
path: frankenphp-linux-${{ matrix.platform == 'linux/amd64' && 'x86_64' || 'aarch64' }}${{ matrix.debug && '-debug' || '' }}${{ matrix.mimalloc && '-mimalloc' || '' }}
@@ -214,15 +228,56 @@ jobs:
runs-on: ${{ startsWith(matrix.platform, 'linux/arm') && 'ubuntu-24.04-arm' || 'ubuntu-24.04' }}
needs: [prepare]
steps:
# Inspired by https://gist.github.com/antiphishfish/1e3fbc3f64ef6f1ab2f47457d2da5d9d and https://github.com/apache/flink/blob/master/tools/azure-pipelines/free_disk_space.sh
- name: Free disk space
run: |
set -xe
sudo rm -rf /usr/share/dotnet
sudo rm -rf /usr/share/swift
sudo rm -rf /usr/local/lib/android
sudo rm -rf /opt/ghc
sudo rm -rf /usr/local/.ghcup
sudo rm -rf "/usr/local/share/boost"
sudo rm -rf "$AGENT_TOOLSDIRECTORY"
sudo rm -rf /opt/hostedtoolcache/
sudo rm -rf /usr/local/graalvm/
sudo rm -rf /usr/local/share/powershell
sudo rm -rf /usr/local/share/chromium
sudo rm -rf /usr/local/lib/node_modules
sudo docker image prune --all --force
APT_PARAMS='sudo apt -y -qq -o=Dpkg::Use-Pty=0'
$APT_PARAMS remove -y '^dotnet-.*'
$APT_PARAMS remove -y '^llvm-.*'
$APT_PARAMS remove -y '^php.*'
$APT_PARAMS remove -y '^mongodb-.*'
$APT_PARAMS remove -y '^mysql-.*'
$APT_PARAMS remove -y azure-cli firefox powershell mono-devel libgl1-mesa-dri
$APT_PARAMS autoremove --purge -y
$APT_PARAMS autoclean
$APT_PARAMS clean
- name: Prepare
id: prepare
run: echo "sanitized_platform=${PLATFORM//\//-}" >> "${GITHUB_OUTPUT}"
env:
PLATFORM: ${{ matrix.platform }}
- uses: actions/checkout@v5
- uses: actions/checkout@v6
with:
ref: ${{ needs.prepare.outputs.ref }}
persist-credentials: false
- name: Set VERSION
run: |
if [ "${GITHUB_REF_TYPE}" == "tag" ]; then
export VERSION=${GITHUB_REF_NAME:1}
elif [ "${GITHUB_EVENT_NAME}" == "schedule" ]; then
export VERSION="${REF}"
else
export VERSION=${GITHUB_SHA}
fi
echo "VERSION=${VERSION}" >> "${GITHUB_ENV}"
env:
REF: ${{ needs.prepare.outputs.ref }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
with:
@@ -239,10 +294,10 @@ jobs:
with:
pull: true
load: ${{ !fromJson(needs.prepare.outputs.push) }}
source: .
targets: static-builder-gnu
set: |
${{ (github.event_name == 'pull_request' || matrix.platform == 'linux/arm64') && 'static-builder-gnu.args.NO_COMPRESS=1' || '' }}
static-builder-gnu.args.BUILD_PACKAGES=1
*.tags=
*.platform=${{ matrix.platform }}
*.cache-from=type=gha,scope=${{ needs.prepare.outputs.ref || github.ref }}-static-builder-gnu
@@ -251,7 +306,6 @@ jobs:
${{ fromJson(needs.prepare.outputs.push) && format('*.output=type=image,name={0},push-by-digest=true,name-canonical=true,push=true', env.IMAGE_NAME) || '' }}
env:
SHA: ${{ github.sha }}
VERSION: ${{ (github.ref_type == 'tag' && github.ref_name) || needs.prepare.outputs.ref || 'dev' }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- # Workaround for https://github.com/actions/runner/pull/2477#issuecomment-1501003600
name: Export metadata
@@ -266,7 +320,7 @@ jobs:
METADATA: ${{ steps.build.outputs.metadata }}
- name: Upload metadata
if: fromJson(needs.prepare.outputs.push)
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v6
with:
name: metadata-static-builder-gnu-${{ steps.prepare.outputs.sanitized_platform }}
path: /tmp/metadata-gnu/*
@@ -290,7 +344,7 @@ jobs:
PLATFORM: ${{ matrix.platform }}
- name: Upload artifact
if: ${{ !fromJson(needs.prepare.outputs.push) }}
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v6
with:
name: frankenphp-linux-${{ matrix.platform == 'linux/amd64' && 'x86_64' || 'aarch64' }}-gnu-files
path: gh-output/*
@@ -326,13 +380,13 @@ jobs:
if: fromJson(needs.prepare.outputs.push)
steps:
- name: Download metadata
uses: actions/download-artifact@v5
uses: actions/download-artifact@v7
with:
pattern: metadata-static-builder-musl-*
path: /tmp/metadata
merge-multiple: true
- name: Download GNU metadata
uses: actions/download-artifact@v5
uses: actions/download-artifact@v7
with:
pattern: metadata-static-builder-gnu-*
path: /tmp/metadata-gnu
@@ -384,12 +438,12 @@ jobs:
matrix:
platform: ["arm64", "x86_64"]
name: Build macOS ${{ matrix.platform }} binaries
runs-on: ${{ matrix.platform == 'arm64' && 'macos-14' || 'macos-13' }}
runs-on: ${{ matrix.platform == 'arm64' && 'macos-15' || 'macos-15-intel' }}
needs: [prepare]
env:
HOMEBREW_NO_AUTO_UPDATE: 1
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
with:
ref: ${{ needs.prepare.outputs.ref }}
persist-credentials: false
@@ -419,13 +473,19 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
RELEASE: ${{ (needs.prepare.outputs.ref || github.ref_type == 'tag') && '1' || '' }}
NO_COMPRESS: ${{ github.event_name == 'pull_request' && '1' || '' }}
- name: Upload logs
if: ${{ failure() }}
uses: actions/upload-artifact@v6
with:
path: dist/static-php-cli/log
name: static-php-cli-log-${{ matrix.platform }}-${{ github.sha }}
- if: needs.prepare.outputs.ref || github.ref_type == 'tag'
uses: actions/attest-build-provenance@v3
with:
subject-path: ${{ github.workspace }}/dist/frankenphp-mac-*
- name: Upload artifact
if: github.ref_type == 'branch'
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v6
with:
name: frankenphp-mac-${{ matrix.platform }}
path: dist/frankenphp-mac-${{ matrix.platform }}

View File

@@ -23,7 +23,7 @@ jobs:
tests-linux:
name: Tests (Linux, PHP ${{ matrix.php-versions }})
runs-on: ubuntu-latest
continue-on-error: ${{ matrix.experimental || false }}
continue-on-error: false
strategy:
fail-fast: false
matrix:
@@ -32,12 +32,11 @@ jobs:
- php-versions: "8.3"
- php-versions: "8.4"
- php-versions: "8.5"
experimental: true
env:
GOMAXPROCS: 10
LIBRARY_PATH: ${{ github.workspace }}/watcher/target/lib
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
with:
persist-credentials: false
- uses: actions/setup-go@v6
@@ -83,35 +82,78 @@ jobs:
- name: Run integrations tests
run: ./reload_test.sh
- name: Lint Go code
uses: golangci/golangci-lint-action@v8
if: matrix.php-versions == '8.4'
uses: golangci/golangci-lint-action@v9
if: matrix.php-versions == '8.5'
with:
version: latest
- name: Ensure go.mod is tidy
if: matrix.php-versions == '8.4'
if: matrix.php-versions == '8.5'
run: go mod tidy -diff
- name: Ensure caddy/go.mod is tidy
if: matrix.php-versions == '8.4'
if: matrix.php-versions == '8.5'
run: go mod tidy -diff
working-directory: caddy/
tests-mac:
name: Tests (macOS, PHP 8.4)
runs-on: macos-latest
env:
HOMEBREW_NO_AUTO_UPDATE: 1
integration-tests:
name: Integration Tests (Linux, PHP ${{ matrix.php-versions }})
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
php-versions: ["8.3", "8.4", "8.5"]
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
with:
persist-credentials: false
- uses: actions/setup-go@v6
with:
go-version: "1.25"
cache-dependency-path: |
go.sum
go.sum
caddy/go.sum
- uses: shivammathur/setup-php@v2
with:
php-version: 8.4
php-version: ${{ matrix.php-versions }}
ini-file: development
coverage: none
tools: none
env:
phpts: ts
debug: true
- name: Install PHP development libraries
run: sudo apt-get update && sudo apt-get install -y libkrb5-dev libsodium-dev libargon2-dev
- name: Install xcaddy
run: go install github.com/caddyserver/xcaddy/cmd/xcaddy@latest
- name: Download PHP sources
run: |
PHP_VERSION=$(php -r "echo PHP_VERSION;")
wget -q "https://www.php.net/distributions/php-${PHP_VERSION}.tar.gz"
tar xzf "php-${PHP_VERSION}.tar.gz"
echo "GEN_STUB_SCRIPT=${PWD}/php-${PHP_VERSION}/build/gen_stub.php" >> "${GITHUB_ENV}"
- name: Set CGO flags
run: |
echo "CGO_CFLAGS=$(php-config --includes)" >> "${GITHUB_ENV}"
echo "CGO_LDFLAGS=$(php-config --ldflags) $(php-config --libs)" >> "${GITHUB_ENV}"
- name: Run integration tests
working-directory: internal/extgen/
run: go test -tags integration -v -timeout 30m
tests-mac:
name: Tests (macOS, PHP 8.5)
runs-on: macos-latest
env:
HOMEBREW_NO_AUTO_UPDATE: 1
steps:
- uses: actions/checkout@v6
with:
persist-credentials: false
- uses: actions/setup-go@v6
with:
go-version: "1.25"
cache-dependency-path: |
go.sum
caddy/go.sum
- uses: shivammathur/setup-php@v2
with:
php-version: 8.5
ini-file: development
coverage: none
tools: none

View File

@@ -1 +1 @@
/github/workspace/docs/mercure.md:jwt:65
/github/workspace/docs/mercure.md:jwt:88

View File

@@ -1,4 +1,5 @@
---
no-hard-tabs: false
MD010: false
MD013: false
MD033: false
MD060: false

View File

@@ -110,7 +110,7 @@ RUN go mod download
WORKDIR /go/src/app
COPY --link . ./
# See https://github.com/docker-library/php/blob/master/8.4/trixie/zts/Dockerfile#L57-L59 for PHP values
# See https://github.com/docker-library/php/blob/master/8.5/trixie/zts/Dockerfile#L57-L59 for PHP values
ENV CGO_CFLAGS="-DFRANKENPHP_VERSION=$FRANKENPHP_VERSION $PHP_CFLAGS"
ENV CGO_CPPFLAGS=$PHP_CPPFLAGS
ENV CGO_LDFLAGS="-L/usr/local/lib -lssl -lcrypto -lreadline -largon2 -lcurl -lonig -lz $PHP_LDFLAGS"

View File

@@ -16,21 +16,77 @@ FrankenPHP can also be used as a standalone Go library to embed PHP in any app u
## Getting Started
### Standalone Binary
We provide static FrankenPHP binaries for Linux and macOS
containing [PHP 8.4](https://www.php.net/releases/8.4/en.php) and most popular PHP extensions.
On Windows, use [WSL](https://learn.microsoft.com/windows/wsl/) to run FrankenPHP.
[Download FrankenPHP](https://github.com/php/frankenphp/releases) or copy this line into your
terminal to automatically install the version appropriate for your platform:
### Install Script
You can copy this line into your terminal to automatically
install an appropriate version for your platform:
```console
curl https://frankenphp.dev/install.sh | sh
mv frankenphp /usr/local/bin/
```
### Standalone Binary
We provide static FrankenPHP binaries for development purposes on Linux and macOS
containing [PHP 8.4](https://www.php.net/releases/8.4/en.php) and most popular PHP extensions.
[Download FrankenPHP](https://github.com/php/frankenphp/releases)
**Installing extensions:** Most common extensions are bundled. It's not possible to install more extensions.
### rpm Packages
Our maintainers offer rpm packages for all systems using `dnf`. To install, run:
```console
sudo dnf install https://rpm.henderkes.com/static-php-1-0.noarch.rpm
sudo dnf module enable php-zts:static-8.4 # 8.2-8.5 available
sudo dnf install frankenphp
```
**Installing extensions:** `sudo dnf install php-zts-<extension>`
For extensions not available by default, use [PIE](https://github.com/php/pie):
```console
sudo dnf install pie-zts
sudo pie-zts install asgrim/example-pie-extension
```
### deb Packages
Our maintainers offer deb packages for all systems using `apt`. To install, run:
```console
sudo curl -fsSL https://key.henderkes.com/static-php.gpg -o /usr/share/keyrings/static-php.gpg && \
echo "deb [signed-by=/usr/share/keyrings/static-php.gpg] https://deb.henderkes.com/ stable main" | sudo tee /etc/apt/sources.list.d/static-php.list && \
sudo apt update
sudo apt install frankenphp
```
**Installing extensions:** `sudo apt install php-zts-<extension>`
For extensions not available by default, use [PIE](https://github.com/php/pie):
```console
sudo apt install pie-zts
sudo pie-zts install asgrim/example-pie-extension
```
### Homebrew
FrankenPHP is also available as a [Homebrew](https://brew.sh) package for macOS and Linux.
```console
brew install dunglas/frankenphp/frankenphp
```
**Installing extensions:** Use [PIE](https://github.com/php/pie).
### Usage
To serve the content of the current directory, run:
```console
@@ -43,6 +99,12 @@ You can also run command-line scripts with:
frankenphp php-cli /path/to/your/script.php
```
For the deb and rpm packages, you can also start the systemd service:
```console
sudo systemctl start frankenphp
```
### Docker
Alternatively, [Docker images](https://frankenphp.dev/docs/docker/) are available:
@@ -60,22 +122,6 @@ Go to `https://localhost`, and enjoy!
> Do not attempt to use `https://127.0.0.1`. Use `https://localhost` and accept the self-signed certificate.
> Use the [`SERVER_NAME` environment variable](docs/config.md#environment-variables) to change the domain to use.
### Homebrew
FrankenPHP is also available as a [Homebrew](https://brew.sh) package for macOS and Linux.
To install it:
```console
brew install dunglas/frankenphp/frankenphp
```
To serve the content of the current directory, run:
```console
frankenphp php-server
```
## Docs
- [Classic mode](https://frankenphp.dev/docs/classic/)

View File

@@ -66,12 +66,12 @@ ENV GOTOOLCHAIN=local
RUN apk add --no-cache --virtual .build-deps \
$PHPIZE_DEPS \
argon2-dev \
# Needed for the custom Go build
# Needed for the custom Go build \
bash \
brotli-dev \
coreutils \
curl-dev \
# Needed for the custom Go build
# Needed for the custom Go build \
git \
gnu-libiconv-dev \
libsodium-dev \
@@ -89,20 +89,20 @@ RUN apk add --no-cache --virtual .build-deps \
# Install e-dant/watcher (necessary for file watching)
WORKDIR /usr/local/src/watcher
RUN --mount=type=secret,id=github-token \
if [ -f /run/secrets/github-token ] && [ -s /run/secrets/github-token ]; then \
curl -s -H "Authorization: Bearer $(cat /run/secrets/github-token)" https://api.github.com/repos/e-dant/watcher/releases/latest; \
else \
curl -s https://api.github.com/repos/e-dant/watcher/releases/latest; \
fi | \
grep tarball_url | \
awk '{ print $2 }' | \
sed 's/,$//' | \
sed 's/"//g' | \
xargs curl -L | \
tar xz --strip-components 1 && \
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release && \
cmake --build build && \
cmake --install build
if [ -f /run/secrets/github-token ] && [ -s /run/secrets/github-token ]; then \
curl -s -H "Authorization: Bearer $(cat /run/secrets/github-token)" https://api.github.com/repos/e-dant/watcher/releases/latest; \
else \
curl -s https://api.github.com/repos/e-dant/watcher/releases/latest; \
fi | \
grep tarball_url | \
awk '{ print $2 }' | \
sed 's/,$//' | \
sed 's/"//g' | \
xargs curl -L | \
tar xz --strip-components 1 && \
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release && \
cmake --build build && \
cmake --install build
WORKDIR /go/src/app
@@ -123,7 +123,7 @@ ENV CGO_LDFLAGS="-lssl -lcrypto -lreadline -largon2 -lcurl -lonig -lz $PHP_LDFLA
WORKDIR /go/src/app/caddy/frankenphp
RUN GOBIN=/usr/local/bin \
../../go.sh install -ldflags "-w -s -extldflags '-Wl,-z,stack-size=0x80000' -X 'github.com/caddyserver/caddy/v2.CustomVersion=FrankenPHP $FRANKENPHP_VERSION PHP $PHP_VERSION Caddy'" -buildvcs=true && \
../../go.sh install -ldflags "-w -s -extldflags '-Wl,-z,stack-size=0x80000' -X 'github.com/caddyserver/caddy/v2.CustomVersion=FrankenPHP $FRANKENPHP_VERSION PHP $PHP_VERSION Caddy'" -buildvcs=true && \
setcap cap_net_bind_service=+ep /usr/local/bin/frankenphp && \
([ -z "${NO_COMPRESS}" ] && upx --best /usr/local/bin/frankenphp || true) && \
frankenphp version && \

View File

@@ -1,51 +0,0 @@
package frankenphp
import (
"sync"
"time"
)
type exponentialBackoff struct {
backoff time.Duration
failureCount int
mu sync.RWMutex
maxBackoff time.Duration
minBackoff time.Duration
maxConsecutiveFailures int
}
// recordSuccess resets the backoff and failureCount
func (e *exponentialBackoff) recordSuccess() {
e.mu.Lock()
e.failureCount = 0
e.backoff = e.minBackoff
e.mu.Unlock()
}
// recordFailure increments the failure count and increases the backoff, it returns true if maxConsecutiveFailures has been reached
func (e *exponentialBackoff) recordFailure() bool {
e.mu.Lock()
e.failureCount += 1
if e.backoff < e.minBackoff {
e.backoff = e.minBackoff
}
e.backoff = min(e.backoff*2, e.maxBackoff)
e.mu.Unlock()
return e.maxConsecutiveFailures != -1 && e.failureCount >= e.maxConsecutiveFailures
}
// wait sleeps for the backoff duration if failureCount is non-zero.
// NOTE: this is not tested and should be kept 'obviously correct' (i.e., simple)
func (e *exponentialBackoff) wait() {
e.mu.RLock()
if e.failureCount == 0 {
e.mu.RUnlock()
return
}
e.mu.RUnlock()
time.Sleep(e.backoff)
}

View File

@@ -1,41 +0,0 @@
package frankenphp
import (
"github.com/stretchr/testify/assert"
"testing"
"time"
)
func TestExponentialBackoff_Reset(t *testing.T) {
e := &exponentialBackoff{
maxBackoff: 5 * time.Second,
minBackoff: 500 * time.Millisecond,
maxConsecutiveFailures: 3,
}
assert.False(t, e.recordFailure())
assert.False(t, e.recordFailure())
e.recordSuccess()
e.mu.RLock()
defer e.mu.RUnlock()
assert.Equal(t, 0, e.failureCount, "expected failureCount to be reset to 0")
assert.Equal(t, e.backoff, e.minBackoff, "expected backoff to be reset to minBackoff")
}
func TestExponentialBackoff_Trigger(t *testing.T) {
e := &exponentialBackoff{
maxBackoff: 500 * 3 * time.Millisecond,
minBackoff: 500 * time.Millisecond,
maxConsecutiveFailures: 3,
}
assert.False(t, e.recordFailure())
assert.False(t, e.recordFailure())
assert.True(t, e.recordFailure())
e.mu.RLock()
defer e.mu.RUnlock()
assert.Equal(t, e.failureCount, e.maxConsecutiveFailures, "expected failureCount to be maxConsecutiveFailures")
assert.Equal(t, e.backoff, e.maxBackoff, "expected backoff to be maxBackoff")
}

View File

@@ -1,119 +0,0 @@
#!/bin/bash
set -o errexit
set -x
# Ensure required tools are installed
if ! command -v rpmbuild &>/dev/null; then
echo "Error: rpm-build is required to create RPM packages."
echo "Install it with: sudo dnf install rpm-build"
exit 1
fi
if ! command -v ruby &>/dev/null; then
echo "Error: Ruby is required by FPM."
echo "Install it with: sudo dnf install ruby"
exit 1
fi
if ! command -v fpm &>/dev/null; then
echo "Error: FPM (rubygem-fpm) is required to create RPM packages."
echo "Install it with: sudo gem install fpm"
exit 1
fi
arch="$(uname -m)"
os="$(uname -s | tr '[:upper:]' '[:lower:]')"
bin="frankenphp-${os}-${arch}"
if [ ! -f "dist/$bin" ]; then
echo "Error: dist/$bin not found. Run './build-static.sh' first"
exit 1
fi
version_output="$(dist/"$bin" version)"
frankenphp_version=$(echo "$version_output" | grep -oP 'FrankenPHP\s+\K[^ ]+' || true)
frankenphp_version=${frankenphp_version#v}
if [[ ! "${frankenphp_version}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "Warning: frankenphp_version must be set to X.Y.Z (e.g. 1.5.1), got '${frankenphp_version}'"
echo "Falling back to non-release version 0.0.0"
frankenphp_version=0.0.0
fi
group_preexists=0
user_preexists=0
if getent group frankenphp >/dev/null; then
group_preexists=1
else
sudo groupadd --system frankenphp
fi
if getent passwd frankenphp >/dev/null; then
user_preexists=1
else
sudo useradd --system \
--gid frankenphp \
--create-home \
--home-dir /var/lib/frankenphp \
--shell /usr/sbin/nologin \
--comment "FrankenPHP web server" \
frankenphp
fi
mkdir -p package/empty
mkdir -p package/etc
[ -f ./dist/static-php-cli/source/php-src/php.ini-production ] && cp -f ./dist/static-php-cli/source/php-src/php.ini-production ./package/etc/php.ini
cd dist
iteration=1
glibc_version=$(ldd -v "$bin" | awk '/GLIBC_/ {gsub(/[()]/, "", $2); print $2}' | grep -v GLIBC_PRIVATE | sort -V | tail -n1)
cxxabi_version=$(strings "$bin" | grep -oP 'CXXABI_\d+\.\d+(\.\d+)?' | sort -V | tail -n1)
fpm -s dir -t rpm -n frankenphp -v "${frankenphp_version}" \
--config-files /etc/frankenphp/Caddyfile \
--config-files /etc/frankenphp/php.ini \
--depends "libc.so.6(${glibc_version})(64bit)" \
--depends "libstdc++.so.6(${cxxabi_version})(64bit)" \
--before-install ../package/rhel/preinstall.sh \
--after-install ../package/rhel/postinstall.sh \
--before-remove ../package/rhel/preuninstall.sh \
--after-remove ../package/rhel/postuninstall.sh \
--iteration "${iteration}" \
--rpm-user frankenphp --rpm-group frankenphp \
"${bin}=/usr/bin/frankenphp" \
"../package/rhel/frankenphp.service=/usr/lib/systemd/system/frankenphp.service" \
"../package/Caddyfile=/etc/frankenphp/Caddyfile" \
"../package/content/=/usr/share/frankenphp" \
"../package/etc/php.ini=/etc/frankenphp/php.ini" \
"../package/empty/=/etc/frankenphp/php.d" \
"../package/empty/=/usr/lib/frankenphp/modules" \
"../package/empty/=/var/lib/frankenphp"
glibc_version=$(ldd -v "$bin" | awk '/GLIBC_/ {gsub(/[()]/, "", $2); print $2}' | grep -v GLIBC_PRIVATE | sed 's/GLIBC_//' | sort -V | tail -n1)
cxxabi_version=$(strings "$bin" | grep -oP 'CXXABI_\d+\.\d+(\.\d+)?' | sed 's/CXXABI_//' | sort -V | tail -n1)
fpm -s dir -t deb -n frankenphp -v "${frankenphp_version}" \
--config-files /etc/frankenphp/Caddyfile \
--config-files /etc/frankenphp/php.ini \
--depends "libc6 (>= ${glibc_version})" \
--depends "libstdc++6 (>= ${cxxabi_version})" \
--after-install ../package/debian/postinst.sh \
--before-remove ../package/debian/prerm.sh \
--after-remove ../package/debian/postrm.sh \
--iteration "${iteration}" \
--deb-user frankenphp --deb-group frankenphp \
"${bin}=/usr/bin/frankenphp" \
"../package/debian/frankenphp.service=/usr/lib/systemd/system/frankenphp.service" \
"../package/Caddyfile=/etc/frankenphp/Caddyfile" \
"../package/content/=/usr/share/frankenphp" \
"../package/etc/php.ini=/etc/frankenphp/php.ini" \
"../package/empty/=/etc/frankenphp/php.d" \
"../package/empty/=/usr/lib/frankenphp/modules" \
"../package/empty/=/var/lib/frankenphp"
[ "$user_preexists" -eq 0 ] && sudo userdel frankenphp
[ "$group_preexists" -eq 0 ] && (sudo groupdel frankenphp || true)
cd ..

View File

@@ -8,8 +8,11 @@ if ! type "git" >/dev/null 2>&1; then
exit 1
fi
CURRENT_DIR=$(pwd)
arch="$(uname -m)"
os="$(uname -s | tr '[:upper:]' '[:lower:]')"
[ "$os" = "darwin" ] && os="mac"
# Supported variables:
# - PHP_VERSION: PHP version to build (default: "8.4")
@@ -73,27 +76,8 @@ if [ -z "${PHP_VERSION}" ]; then
export PHP_VERSION
fi
# default extension set
defaultExtensions="amqp,apcu,ast,bcmath,brotli,bz2,calendar,ctype,curl,dba,dom,exif,fileinfo,filter,ftp,gd,gmp,gettext,iconv,igbinary,imagick,intl,ldap,lz4,mbregex,mbstring,memcache,memcached,mysqli,mysqlnd,opcache,openssl,password-argon2,parallel,pcntl,pdo,pdo_mysql,pdo_pgsql,pdo_sqlite,pdo_sqlsrv,pgsql,phar,posix,protobuf,readline,redis,session,shmop,simplexml,soap,sockets,sodium,sqlite3,ssh2,sysvmsg,sysvsem,sysvshm,tidy,tokenizer,xlswriter,xml,xmlreader,xmlwriter,xz,zip,zlib,yaml,zstd"
defaultExtensionLibs="libavif,nghttp2,nghttp3,ngtcp2"
md5binary="md5sum"
if [ "${os}" = "darwin" ]; then
os="mac"
md5binary="md5 -q"
fi
if [ "${os}" = "linux" ] && ! type "cmake" >/dev/null 2>&1; then
echo "The \"cmake\" command must be installed."
exit 1
fi
if [ "${os}" = "linux" ] && { [[ "${arch}" =~ "aarch" ]] || [[ "${arch}" =~ "arm" ]]; }; then
fpic="-fPIC"
fpie="-fPIE"
else
fpic="-fpic"
fpie="-fpie"
fi
defaultExtensions="amqp,apcu,ast,bcmath,brotli,bz2,calendar,ctype,curl,dba,dom,exif,fileinfo,filter,ftp,gd,gmp,gettext,iconv,igbinary,imagick,intl,ldap,lz4,mbregex,mbstring,memcache,memcached,mysqli,mysqlnd,opcache,openssl,password-argon2,parallel,pcntl,pdo,pdo_mysql,pdo_pgsql,pdo_sqlite,pdo_sqlsrv,pgsql,phar,posix,protobuf,readline,redis,session,shmop,simplexml,soap,sockets,sodium,sqlite3,ssh2,sysvmsg,sysvsem,sysvshm,tidy,tokenizer,xlswriter,xml,xmlreader,xmlwriter,xsl,xz,zip,zlib,yaml,zstd"
defaultExtensionLibs="libavif,nghttp2,nghttp3,ngtcp2,watcher"
if [ -z "${FRANKENPHP_VERSION}" ]; then
FRANKENPHP_VERSION="$(git rev-parse --verify HEAD)"
@@ -115,8 +99,6 @@ elif [ -d ".git/" ]; then
fi
fi
bin="frankenphp-${os}-${arch}"
if [ -n "${CLEAN}" ]; then
rm -Rf dist/
go clean -cache
@@ -194,153 +176,51 @@ if [ -n "${MIMALLOC}" ]; then
fi
fi
# Build libphp if necessary
cache_key="${PHP_VERSION}-${PHP_EXTENSIONS}-${PHP_EXTENSION_LIBS}"
if [ -f ../cache_key ] && [ "$(cat ../cache_key)" = "${cache_key}" ] && [ -f "buildroot/lib/libphp.a" ]; then
echo "Hit cache, skipping libphp build."
else
${spcCommand} doctor --auto-fix
# shellcheck disable=SC2086
${spcCommand} download --with-php="${PHP_VERSION}" --for-extensions="${PHP_EXTENSIONS}" --for-libs="${PHP_EXTENSION_LIBS}" ${SPC_OPT_DOWNLOAD_ARGS}
# shellcheck disable=SC2086
${spcCommand} build --enable-zts --build-embed ${SPC_OPT_BUILD_ARGS} "${PHP_EXTENSIONS}" --with-libs="${PHP_EXTENSION_LIBS}"
echo -n "${cache_key}" >../cache_key
fi
if ! type "go" >/dev/null 2>&1; then
echo "The \"go\" command must be installed."
exit 1
fi
XCADDY_COMMAND="xcaddy"
if ! type "$XCADDY_COMMAND" >/dev/null 2>&1; then
go install github.com/caddyserver/xcaddy/cmd/xcaddy@latest
XCADDY_COMMAND="$(go env GOPATH)/bin/xcaddy"
fi
curlGitHubHeaders=(--header "X-GitHub-Api-Version: 2022-11-28")
if [ "${GITHUB_TOKEN}" ]; then
curlGitHubHeaders+=(--header "Authorization: Bearer ${GITHUB_TOKEN}")
fi
# Compile e-dant/watcher as a static library
mkdir -p watcher
cd watcher
curl -f --retry 5 "${curlGitHubHeaders[@]}" https://api.github.com/repos/e-dant/watcher/releases/latest |
grep tarball_url |
awk '{ print $2 }' |
sed 's/,$//' |
sed 's/"//g' |
xargs curl -fL --retry 5 "${curlGitHubHeaders[@]}" |
tar xz --strip-components 1
cd watcher-c
if [ -z "${CC}" ]; then
watcherCC=cc
else
watcherCC="${CC}"
fi
${watcherCC} -c -o libwatcher-c.o ./src/watcher-c.cpp -I ./include -I ../include -std=c++17 -Wall -Wextra "${fpic}"
ar rcs libwatcher-c.a libwatcher-c.o
cp libwatcher-c.a ../../buildroot/lib/libwatcher-c.a
mkdir -p ../../buildroot/include/wtr
cp -R include/wtr/watcher-c.h ../../buildroot/include/wtr/watcher-c.h
cd ../../
# See https://github.com/docker-library/php/blob/master/8.3/alpine3.20/zts/Dockerfile#L53-L55
CGO_CFLAGS="-DFRANKENPHP_VERSION=${FRANKENPHP_VERSION} -I${PWD}/buildroot/include/ $(${spcCommand} spc-config "${PHP_EXTENSIONS}" --with-libs="${PHP_EXTENSION_LIBS}" --includes)"
if [ -n "${DEBUG_SYMBOLS}" ]; then
CGO_CFLAGS="-g ${CGO_CFLAGS}"
else
CGO_CFLAGS="-fstack-protector-strong ${fpic} ${fpie} -O2 -D_LARGEFILE_SOURCE -D_FILE_OFFSET_BITS=64 ${CGO_CFLAGS}"
fi
export CGO_CFLAGS
export CGO_CPPFLAGS="${CGO_CFLAGS}"
if [ "${os}" = "mac" ]; then
export CGO_LDFLAGS="-framework CoreFoundation -framework SystemConfiguration"
elif [ "${os}" = "linux" ] && [ -z "${DEBUG_SYMBOLS}" ]; then
CGO_LDFLAGS="-Wl,-O1 -pie"
fi
if [ "${os}" = "linux" ] && [ "${SPC_LIBC}" = "glibc" ]; then
CGO_LDFLAGS="${CGO_LDFLAGS} -Wl,--allow-multiple-definition -Wl,--export-dynamic"
fi
CGO_LDFLAGS="${CGO_LDFLAGS} ${PWD}/buildroot/lib/libbrotlicommon.a ${PWD}/buildroot/lib/libbrotlienc.a ${PWD}/buildroot/lib/libbrotlidec.a ${PWD}/buildroot/lib/libwatcher-c.a $(${spcCommand} spc-config "${PHP_EXTENSIONS}" --with-libs="${PHP_EXTENSION_LIBS}" --libs)"
if [[ "$CGO_LDFLAGS" == *"${PWD}/buildroot/lib/mimalloc.o"* ]]; then
CGO_LDFLAGS=${CGO_LDFLAGS//${PWD}\/buildroot\/lib\/mimalloc.o/}
CGO_LDFLAGS="${PWD}/buildroot/lib/libmimalloc.a $CGO_LDFLAGS"
fi
if [ "${os}" = "linux" ] && [ "${SPC_LIBC}" = "glibc" ]; then
CGO_LDFLAGS="${CGO_LDFLAGS//-lphp/-Wl,--whole-archive -lphp -Wl,--no-whole-archive}"
# shellcheck disable=SC2046
ar d "${PWD}/buildroot/lib/libphp.a" $(ar t "${PWD}/buildroot/lib/libphp.a" | grep '\.a$')
fi
export CGO_LDFLAGS
LIBPHP_VERSION="$(./buildroot/bin/php-config --version)"
cd ../
if [ -z "${DEBUG_SYMBOLS}" ]; then
extraLdflags="-w -s"
fi
cd ../
# Embed PHP app, if any
if [ -n "${EMBED}" ] && [ -d "${EMBED}" ]; then
tar -cf app.tar -C "${EMBED}" .
${md5binary} app.tar | awk '{printf $1}' >app_checksum.txt
SPC_OPT_BUILD_ARGS="${SPC_OPT_BUILD_ARGS} --with-frankenphp-app=${EMBED}"
fi
if [ -z "${XCADDY_ARGS}" ]; then
XCADDY_ARGS="--with github.com/dunglas/caddy-cbrotli --with github.com/dunglas/mercure/caddy --with github.com/dunglas/vulcain/caddy"
SPC_OPT_INSTALL_ARGS="go-xcaddy"
if [ -z "${DEBUG_SYMBOLS}" ] && [ -z "${NO_COMPRESS}" ] && [ "${os}" = "linux" ]; then
SPC_OPT_BUILD_ARGS="${SPC_OPT_BUILD_ARGS} --with-upx-pack"
SPC_OPT_INSTALL_ARGS="${SPC_OPT_INSTALL_ARGS} upx"
fi
XCADDY_DEBUG=0
export SPC_DEFAULT_C_FLAGS="-fPIC -O2"
if [ -n "${DEBUG_SYMBOLS}" ]; then
XCADDY_DEBUG=1
fi
if [ "${SPC_LIBC}" = "musl" ]; then
muslStackSizeFix="-Wl,-z,stack-size=0x80000"
fi
go env
cd caddy/
if [ -z "${SPC_LIBC}" ] || [ "${SPC_LIBC}" = "musl" ]; then
xcaddyGoBuildFlags="-buildmode=pie -tags cgo,netgo,osusergo,static_build,nobadger,nomysql,nopgx -ldflags \"-linkmode=external -extldflags '-static-pie ${muslStackSizeFix}' ${extraLdflags} -X 'github.com/caddyserver/caddy/v2.CustomVersion=FrankenPHP ${FRANKENPHP_VERSION} PHP ${LIBPHP_VERSION} Caddy'\""
elif [ "${SPC_LIBC}" = "glibc" ]; then
xcaddyGoBuildFlags="-buildmode=pie -tags cgo,netgo,osusergo,nobadger,nomysql,nopgx -ldflags \"-linkmode=external -extldflags '-pie' ${extraLdflags} -X 'github.com/caddyserver/caddy/v2.CustomVersion=FrankenPHP ${FRANKENPHP_VERSION} PHP ${LIBPHP_VERSION} Caddy'\""
SPC_CMD_VAR_PHP_MAKE_EXTRA_CFLAGS="${SPC_CMD_VAR_PHP_MAKE_EXTRA_CFLAGS} -fPIE -g"
else
SPC_CMD_VAR_PHP_MAKE_EXTRA_CFLAGS="${SPC_CMD_VAR_PHP_MAKE_EXTRA_CFLAGS} -fPIE -fstack-protector-strong -O2 -w -s"
fi
export SPC_CMD_VAR_PHP_MAKE_EXTRA_CFLAGS
export SPC_CMD_VAR_FRANKENPHP_XCADDY_MODULES="--with github.com/dunglas/mercure/caddy --with github.com/dunglas/vulcain/caddy --with github.com/dunglas/caddy-cbrotli"
# Build FrankenPHP
${spcCommand} doctor --auto-fix
for pkg in ${SPC_OPT_INSTALL_ARGS}; do
${spcCommand} install-pkg "${pkg}"
done
# shellcheck disable=SC2086
CGO_ENABLED=1 \
XCADDY_GO_BUILD_FLAGS=${xcaddyGoBuildFlags} \
XCADDY_DEBUG="${XCADDY_DEBUG}" \
${XCADDY_COMMAND} build \
--output "../dist/${bin}" \
${XCADDY_ARGS} \
--with github.com/dunglas/frankenphp=.. \
--with github.com/dunglas/frankenphp/caddy=.
cd ..
${spcCommand} download --with-php="${PHP_VERSION}" --for-extensions="${PHP_EXTENSIONS}" --for-libs="${PHP_EXTENSION_LIBS}" ${SPC_OPT_DOWNLOAD_ARGS}
export FRANKENPHP_SOURCE_PATH="${CURRENT_DIR}"
# shellcheck disable=SC2086
${spcCommand} build --enable-zts --build-embed --build-frankenphp ${SPC_OPT_BUILD_ARGS} "${PHP_EXTENSIONS}" --with-libs="${PHP_EXTENSION_LIBS}"
if [ -d "${EMBED}" ]; then
truncate -s 0 app.tar
truncate -s 0 app_checksum.txt
if [ -n "$CI" ]; then
rm -rf ./downloads
rm -rf ./source
fi
if type "upx" >/dev/null 2>&1 && [ -z "${DEBUG_SYMBOLS}" ] && [ -z "${NO_COMPRESS}" ]; then
upx --best "dist/${bin}"
fi
cd ../..
"dist/${bin}" version
"dist/${bin}" build-info
bin="dist/frankenphp-${os}-${arch}"
cp "dist/static-php-cli/buildroot/bin/frankenphp" "${bin}"
"${bin}" version
"${bin}" build-info
if [ -n "${RELEASE}" ]; then
gh release upload "v${FRANKENPHP_VERSION}" "dist/${bin}" --repo dunglas/frankenphp --clobber
gh release upload "v${FRANKENPHP_VERSION}" "${bin}" --repo dunglas/frankenphp --clobber
fi
if [ -n "${CURRENT_REF}" ]; then

View File

@@ -4,12 +4,13 @@ import (
"bytes"
"encoding/json"
"fmt"
"github.com/dunglas/frankenphp/internal/fastabs"
"io"
"net/http"
"sync"
"testing"
"github.com/dunglas/frankenphp/internal/fastabs"
"github.com/caddyserver/caddy/v2/caddytest"
"github.com/dunglas/frankenphp"
"github.com/stretchr/testify/assert"
@@ -94,7 +95,10 @@ func TestAutoScaleWorkerThreads(t *testing.T) {
frankenphp {
max_threads 10
num_threads 2
worker ../testdata/sleep.php 1
worker ../testdata/sleep.php {
num 1
max_threads 3
}
}
}
@@ -128,8 +132,8 @@ func TestAutoScaleWorkerThreads(t *testing.T) {
}
}
// assert that there are now more threads than before
assert.NotEqual(t, amountOfThreads, 2)
assert.NotEqual(t, amountOfThreads, 2, "at least one thread should have been auto-scaled")
assert.LessOrEqual(t, amountOfThreads, 4, "at most 3 max_threads + 1 regular thread should be present")
}
// Note this test requires at least 2x40MB available memory for the process

View File

@@ -1,12 +1,14 @@
package caddy
import (
"context"
"errors"
"fmt"
"log/slog"
"path/filepath"
"strconv"
"strings"
"sync"
"time"
"github.com/caddyserver/caddy/v2"
@@ -18,6 +20,22 @@ import (
"github.com/dunglas/frankenphp/internal/fastabs"
)
var (
options []frankenphp.Option
optionsMU sync.RWMutex
)
// EXPERIMENTAL: RegisterWorkers provides a way for extensions to register frankenphp.Workers
func RegisterWorkers(name, fileName string, num int, wo ...frankenphp.WorkerOption) frankenphp.Workers {
w, opt := frankenphp.WithExtensionWorkers(name, fileName, num, wo...)
optionsMU.Lock()
options = append(options, opt)
optionsMU.Unlock()
return w
}
// FrankenPHPApp represents the global "frankenphp" directive in the Caddyfile
// it's responsible for starting up the global PHP instance and all threads
//
@@ -31,14 +49,16 @@ type FrankenPHPApp struct {
NumThreads int `json:"num_threads,omitempty"`
// MaxThreads limits how many threads can be started at runtime. Default 2x NumThreads
MaxThreads int `json:"max_threads,omitempty"`
// Workers configures the worker scripts to start.
// Workers configures the worker scripts to start
Workers []workerConfig `json:"workers,omitempty"`
// Overwrites the default php ini configuration
PhpIni map[string]string `json:"php_ini,omitempty"`
// The maximum amount of time a request may be stalled waiting for a thread
MaxWaitTime time.Duration `json:"max_wait_time,omitempty"`
opts []frankenphp.Option
metrics frankenphp.Metrics
ctx context.Context
logger *slog.Logger
}
@@ -54,8 +74,12 @@ func (f FrankenPHPApp) CaddyModule() caddy.ModuleInfo {
// Provision sets up the module.
func (f *FrankenPHPApp) Provision(ctx caddy.Context) error {
f.ctx = ctx
f.logger = ctx.Slogger()
// We have at least 7 hardcoded options
f.opts = make([]frankenphp.Option, 0, 7+len(options))
if httpApp, err := ctx.AppIfConfigured("http"); err == nil {
if httpApp.(*caddyhttp.App).Metrics != nil {
f.metrics = frankenphp.NewPrometheusMetrics(ctx.GetMetricsRegistry())
@@ -94,42 +118,54 @@ retry:
func (f *FrankenPHPApp) addModuleWorkers(workers ...workerConfig) ([]workerConfig, error) {
for i := range workers {
w := &workers[i]
if frankenphp.EmbeddedAppPath != "" && filepath.IsLocal(w.FileName) {
w.FileName = filepath.Join(frankenphp.EmbeddedAppPath, w.FileName)
}
if w.Name == "" {
w.Name = f.generateUniqueModuleWorkerName(w.FileName)
} else if !strings.HasPrefix(w.Name, "m#") {
w.Name = "m#" + w.Name
}
f.Workers = append(f.Workers, *w)
}
return workers, nil
}
func (f *FrankenPHPApp) Start() error {
repl := caddy.NewReplacer()
opts := []frankenphp.Option{
optionsMU.RLock()
f.opts = append(f.opts, options...)
optionsMU.RUnlock()
f.opts = append(f.opts,
frankenphp.WithContext(f.ctx),
frankenphp.WithLogger(f.logger),
frankenphp.WithNumThreads(f.NumThreads),
frankenphp.WithMaxThreads(f.MaxThreads),
frankenphp.WithLogger(f.logger),
frankenphp.WithMetrics(f.metrics),
frankenphp.WithPhpIni(f.PhpIni),
frankenphp.WithMaxWaitTime(f.MaxWaitTime),
}
for _, w := range append(f.Workers) {
workerOpts := []frankenphp.WorkerOption{
)
for _, w := range f.Workers {
w.options = append(w.options,
frankenphp.WithWorkerEnv(w.Env),
frankenphp.WithWorkerWatchMode(w.Watch),
frankenphp.WithWorkerMaxFailures(w.MaxConsecutiveFailures),
}
frankenphp.WithWorkerMaxThreads(w.MaxThreads),
frankenphp.WithWorkerRequestOptions(w.requestOptions...),
)
opts = append(opts, frankenphp.WithWorkers(w.Name, repl.ReplaceKnown(w.FileName, ""), w.Num, workerOpts...))
f.opts = append(f.opts, frankenphp.WithWorkers(w.Name, repl.ReplaceKnown(w.FileName, ""), w.Num, w.options...))
}
frankenphp.Shutdown()
if err := frankenphp.Init(opts...); err != nil {
if err := frankenphp.Init(f.opts...); err != nil {
return err
}
@@ -137,7 +173,11 @@ func (f *FrankenPHPApp) Start() error {
}
func (f *FrankenPHPApp) Stop() error {
f.logger.Info("FrankenPHP stopped 🐘")
ctx := caddy.ActiveContext()
if f.logger.Enabled(caddy.ActiveContext(), slog.LevelInfo) {
f.logger.LogAttrs(ctx, slog.LevelInfo, "FrankenPHP stopped 🐘")
}
// attempt a graceful shutdown if caddy is exiting
// note: Exiting() is currently marked as 'experimental'
@@ -151,6 +191,10 @@ func (f *FrankenPHPApp) Stop() error {
f.NumThreads = 0
f.MaxWaitTime = 0
optionsMU.Lock()
options = nil
optionsMU.Unlock()
return nil
}
@@ -235,7 +279,7 @@ func (f *FrankenPHPApp) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
}
case "worker":
wc, err := parseWorkerConfig(d)
wc, err := unmarshalWorker(d)
if err != nil {
return err
}
@@ -254,8 +298,7 @@ func (f *FrankenPHPApp) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
f.Workers = append(f.Workers, wc)
default:
allowedDirectives := "num_threads, max_threads, php_ini, worker, max_wait_time"
return wrongSubDirectiveError("frankenphp", allowedDirectives, d.Val())
return wrongSubDirectiveError("frankenphp", "num_threads, max_threads, php_ini, worker, max_wait_time", d.Val())
}
}
}

View File

@@ -12,7 +12,7 @@ import (
const (
defaultDocumentRoot = "public"
defaultWatchPattern = "./**/*.{php,yaml,yml,twig,env}"
defaultWatchPattern = "./**/*.{env,php,twig,yaml,yml}"
)
func init() {
@@ -29,7 +29,7 @@ func init() {
httpcaddyfile.RegisterDirectiveOrder("php_server", "before", "file_server")
}
// return a nice error message
func wrongSubDirectiveError(module string, allowedDriectives string, wrongValue string) error {
return fmt.Errorf("unknown '%s' subdirective: '%s' (allowed directives are: %s)", module, wrongValue, allowedDriectives)
// wrongSubDirectiveError returns a nice error message.
func wrongSubDirectiveError(module string, allowedDirectives string, wrongValue string) error {
return fmt.Errorf("unknown %q subdirective: %s (allowed directives are: %s)", module, wrongValue, allowedDirectives)
}

View File

@@ -168,6 +168,28 @@ func TestGlobalAndModuleWorker(t *testing.T) {
wg.Wait()
}
func TestModuleWorkerInheritsEnv(t *testing.T) {
tester := caddytest.NewTester(t)
tester.InitServer(`
{
skip_install_trust
admin localhost:2999
}
http://localhost:`+testPort+` {
route {
php {
root ../testdata
env APP_ENV inherit_this
worker worker-with-env.php
}
}
}
`, "caddyfile")
tester.AssertGetResponse("http://localhost:"+testPort+"/worker-with-env.php", http.StatusOK, "Worker has APP_ENV=inherit_this")
}
func TestNamedModuleWorkers(t *testing.T) {
var wg sync.WaitGroup
testPortNum, _ := strconv.Atoi(testPort)
@@ -385,7 +407,7 @@ func TestPHPServerDirective(t *testing.T) {
`, "caddyfile")
tester.AssertGetResponse("http://localhost:"+testPort, http.StatusOK, "I am by birth a Genevese (i not set)")
tester.AssertGetResponse("http://localhost:"+testPort+"/hello.txt", http.StatusOK, "Hello")
tester.AssertGetResponse("http://localhost:"+testPort+"/hello.txt", http.StatusOK, "Hello\n")
tester.AssertGetResponse("http://localhost:"+testPort+"/not-found.txt", http.StatusOK, "I am by birth a Genevese (i not set)")
}
@@ -943,7 +965,7 @@ func TestMaxWaitTime(t *testing.T) {
for range 10 {
go func() {
statusCode := getStatusCode("http://localhost:"+testPort+"/sleep.php?sleep=10", t)
if statusCode == http.StatusGatewayTimeout {
if statusCode == http.StatusServiceUnavailable {
success.Store(true)
}
wg.Done()
@@ -951,7 +973,7 @@ func TestMaxWaitTime(t *testing.T) {
}
wg.Wait()
require.True(t, success.Load(), "At least one request should have failed with a 504 Gateway Timeout status")
require.True(t, success.Load(), "At least one request should have failed with a 503 Service Unavailable status")
}
func TestMaxWaitTimeWorker(t *testing.T) {
@@ -990,23 +1012,26 @@ func TestMaxWaitTimeWorker(t *testing.T) {
for range 10 {
go func() {
statusCode := getStatusCode("http://localhost:"+testPort+"/sleep.php?sleep=10&iteration=1", t)
if statusCode == http.StatusGatewayTimeout {
if statusCode == http.StatusServiceUnavailable {
success.Store(true)
}
wg.Done()
}()
}
wg.Wait()
require.True(t, success.Load(), "At least one request should have failed with a 504 Gateway Timeout status")
require.True(t, success.Load(), "At least one request should have failed with a 503 Service Unavailable status")
// Fetch metrics
resp, err := http.Get("http://localhost:2999/metrics")
require.NoError(t, err, "failed to fetch metrics")
defer resp.Body.Close()
t.Cleanup(func() {
require.NoError(t, resp.Body.Close())
})
// Read and parse metrics
metrics := new(bytes.Buffer)
_, err = metrics.ReadFrom(resp.Body)
require.NoError(t, err)
expectedMetrics := `
# TYPE frankenphp_worker_queue_depth gauge
@@ -1337,7 +1362,7 @@ func TestWorkerMatchDirective(t *testing.T) {
}
`, "caddyfile")
// worker is outside of public directory, match anyways
// worker is outside public directory, match anyway
tester.AssertGetResponse("http://localhost:"+testPort+"/matched-path", http.StatusOK, "requests:1")
tester.AssertGetResponse("http://localhost:"+testPort+"/matched-path/anywhere", http.StatusOK, "requests:2")
@@ -1423,3 +1448,55 @@ func TestWorkerMatchDirectiveWithoutFileServer(t *testing.T) {
// the request should completely fall through the php_server module
tester.AssertGetResponse("http://localhost:"+testPort+"/static.txt", http.StatusNotFound, "Request falls through")
}
func TestDd(t *testing.T) {
tester := caddytest.NewTester(t)
tester.InitServer(`
{
skip_install_trust
admin localhost:2999
}
http://localhost:`+testPort+` {
php {
worker ../testdata/dd.php 1 {
match *
}
}
`, "caddyfile")
// simulate Symfony's dd()
tester.AssertGetResponse(
"http://localhost:"+testPort+"/some-path?output=dump123",
http.StatusInternalServerError,
"dump123",
)
}
func TestLog(t *testing.T) {
tester := caddytest.NewTester(t)
tester.InitServer(`
{
skip_install_trust
admin localhost:2999
}
http://localhost:`+testPort+` {
log {
output stdout
format json
}
root ../testdata
php_server {
worker ../testdata/log-frankenphp_log.php
}
}
`, "caddyfile")
tester.AssertGetResponse(
"http://localhost:"+testPort+"/log-frankenphp_log.php?i=0",
http.StatusOK,
"",
)
}

View File

@@ -181,7 +181,7 @@ func TestModuleWorkerWithWatchConfiguration(t *testing.T) {
// Verify that the watch directories were set correctly
require.Len(t, module.Workers[0].Watch, 3, "Expected three watch patterns")
require.Equal(t, "./**/*.{php,yaml,yml,twig,env}", module.Workers[0].Watch[0], "First watch pattern should be the default")
require.Equal(t, defaultWatchPattern, module.Workers[0].Watch[0], "First watch pattern should be the default")
require.Equal(t, "./src/**/*.php", module.Workers[0].Watch[1], "Second watch pattern should match the configuration")
require.Equal(t, "./config/**/*.yaml", module.Workers[0].Watch[2], "Third watch pattern should match the configuration")
}

View File

@@ -30,8 +30,6 @@
# Uncomment the following lines to enable Mercure and Vulcain modules
#mercure {
# # Publisher JWT key
# transport_url {$MERCURE_TRANSPORT_URL:bolt:///data/mercure.db}
# # Publisher JWT key
# publisher_jwt {env.MERCURE_PUBLISHER_JWT_KEY} {env.MERCURE_PUBLISHER_JWT_ALG}
# # Subscriber JWT key
# subscriber_jwt {env.MERCURE_SUBSCRIBER_JWT_KEY} {env.MERCURE_SUBSCRIBER_JWT_ALG}

View File

@@ -1,6 +1,6 @@
module github.com/dunglas/frankenphp/caddy
go 1.25.0
go 1.25.4
replace github.com/dunglas/frankenphp => ../
@@ -10,47 +10,47 @@ require (
github.com/caddyserver/caddy/v2 v2.10.2
github.com/caddyserver/certmagic v0.25.0
github.com/dunglas/caddy-cbrotli v1.0.1
github.com/dunglas/frankenphp v1.9.1
github.com/dunglas/mercure/caddy v0.20.2
github.com/dunglas/frankenphp v1.11.1
github.com/dunglas/mercure v0.21.4
github.com/dunglas/mercure/caddy v0.21.4
github.com/dunglas/vulcain/caddy v1.2.1
github.com/prometheus/client_golang v1.23.2
github.com/spf13/cobra v1.10.1
github.com/spf13/cobra v1.10.2
github.com/stretchr/testify v1.11.1
)
require github.com/smallstep/go-attestation v0.4.4-0.20241119153605-2306d5b464ca // indirect
require (
cel.dev/expr v0.24.0 // indirect
cloud.google.com/go/auth v0.16.5 // indirect
cel.dev/expr v0.25.1 // indirect
cloud.google.com/go/auth v0.18.0 // indirect
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
cloud.google.com/go/compute/metadata v0.8.0 // indirect
cloud.google.com/go/compute/metadata v0.9.0 // indirect
dario.cat/mergo v1.0.2 // indirect
filippo.io/edwards25519 v1.1.0 // indirect
github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96 // indirect
github.com/BurntSushi/toml v1.5.0 // indirect
github.com/KimMachineGun/automemlimit v0.7.4 // indirect
github.com/KimMachineGun/automemlimit v0.7.5 // indirect
github.com/Masterminds/goutils v1.1.1 // indirect
github.com/Masterminds/semver/v3 v3.4.0 // indirect
github.com/Masterminds/sprig/v3 v3.3.0 // indirect
github.com/MauriceGit/skiplist v0.0.0-20211105230623-77f5c8d3e145 // indirect
github.com/MicahParks/jwkset v0.9.6 // indirect
github.com/MicahParks/keyfunc/v3 v3.6.1 // indirect
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/RoaringBitmap/roaring v1.9.4 // indirect
github.com/alecthomas/chroma/v2 v2.20.0 // indirect
github.com/MicahParks/jwkset v0.11.0 // indirect
github.com/MicahParks/keyfunc/v3 v3.7.0 // indirect
github.com/RoaringBitmap/roaring/v2 v2.14.4 // indirect
github.com/alecthomas/chroma/v2 v2.21.0 // indirect
github.com/antlr4-go/antlr/v4 v4.13.1 // indirect
github.com/aryann/difflib v0.0.0-20210328193216-ff5ff6dc229b // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/bits-and-blooms/bitset v1.24.0 // indirect
github.com/bits-and-blooms/bitset v1.24.4 // indirect
github.com/caddyserver/zerossl v0.1.3 // indirect
github.com/ccoveille/go-safecast v1.6.1 // indirect
github.com/ccoveille/go-safecast/v2 v2.0.0 // indirect
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
github.com/cespare/xxhash v1.1.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/chzyer/readline v1.5.1 // indirect
github.com/cloudflare/circl v1.6.1 // indirect
github.com/coreos/go-oidc/v3 v3.15.0 // indirect
github.com/coreos/go-oidc/v3 v3.17.0 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/dgraph-io/badger v1.6.2 // indirect
@@ -58,64 +58,60 @@ require (
github.com/dgraph-io/ristretto v0.2.0 // indirect
github.com/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da // indirect
github.com/dlclark/regexp2 v1.11.5 // indirect
github.com/dolthub/maphash v0.1.0 // indirect
github.com/dunglas/httpsfv v1.1.0 // indirect
github.com/dunglas/mercure v0.20.2 // indirect
github.com/dunglas/skipfilter v1.0.0 // indirect
github.com/dunglas/vulcain v1.2.1 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/e-dant/watcher/watcher-go v0.0.0-20251208164151-f88ec3b7e146 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/francoispqt/gojay v1.2.13 // indirect
github.com/fsnotify/fsnotify v1.9.0 // indirect
github.com/fxamacker/cbor/v2 v2.9.0 // indirect
github.com/gammazero/deque v1.1.0 // indirect
github.com/getkin/kin-openapi v0.133.0 // indirect
github.com/go-chi/chi/v5 v5.2.3 // indirect
github.com/go-jose/go-jose/v3 v3.0.4 // indirect
github.com/go-jose/go-jose/v4 v4.1.2 // indirect
github.com/go-jose/go-jose/v4 v4.1.3 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-openapi/jsonpointer v0.21.2 // indirect
github.com/go-openapi/swag v0.23.1 // indirect
github.com/go-openapi/jsonpointer v0.22.4 // indirect
github.com/go-openapi/swag/jsonname v0.25.4 // indirect
github.com/go-sql-driver/mysql v1.9.3 // indirect
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
github.com/gofrs/uuid v4.4.0+incompatible // indirect
github.com/gofrs/uuid/v5 v5.3.2 // indirect
github.com/gofrs/uuid/v5 v5.4.0 // indirect
github.com/golang-jwt/jwt/v5 v5.3.0 // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/golang/snappy v1.0.0 // indirect
github.com/google/brotli/go/cbrotli v1.1.0 // indirect
github.com/google/cel-go v0.26.1 // indirect
github.com/google/certificate-transparency-go v1.3.2 // indirect
github.com/google/go-tpm v0.9.5 // indirect
github.com/google/go-tpm v0.9.7 // indirect
github.com/google/go-tspi v0.3.0 // indirect
github.com/google/s2a-go v0.1.9 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.7 // indirect
github.com/googleapis/gax-go/v2 v2.15.0 // indirect
github.com/gorilla/handlers v1.5.2 // indirect
github.com/gorilla/mux v1.8.1 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 // indirect
github.com/hashicorp/golang-lru v1.0.2 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 // indirect
github.com/huandu/xstrings v1.5.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/pgx/v5 v5.7.5 // indirect
github.com/jackc/pgx/v5 v5.7.6 // indirect
github.com/jackc/puddle/v2 v2.2.2 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/kevburnsjr/skipfilter v0.0.1 // indirect
github.com/klauspost/compress v1.18.0 // indirect
github.com/klauspost/compress v1.18.2 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/kylelemons/godebug v1.1.0 // indirect
github.com/libdns/libdns v1.1.1 // indirect
github.com/mailru/easyjson v0.9.0 // indirect
github.com/mailru/easyjson v0.9.1 // indirect
github.com/manifoldco/promptui v0.9.0 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/maypok86/otter v1.2.4 // indirect
github.com/maypok86/otter/v2 v2.2.1 // indirect
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
github.com/mholt/acmez/v3 v3.1.3 // indirect
github.com/miekg/dns v1.1.68 // indirect
github.com/mholt/acmez/v3 v3.1.4 // indirect
github.com/miekg/dns v1.1.69 // indirect
github.com/mitchellh/copystructure v1.2.0 // indirect
github.com/mitchellh/go-ps v1.0.0 // indirect
github.com/mitchellh/reflectwalk v1.0.2 // indirect
@@ -131,34 +127,35 @@ require (
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.66.1 // indirect
github.com/prometheus/procfs v0.17.0 // indirect
github.com/quic-go/qpack v0.5.1 // indirect
github.com/quic-go/quic-go v0.54.0 // indirect
github.com/prometheus/common v0.67.4 // indirect
github.com/prometheus/procfs v0.19.2 // indirect
github.com/quic-go/qpack v0.6.0 // indirect
github.com/quic-go/quic-go v0.57.1 // indirect
github.com/rs/cors v1.11.1 // indirect
github.com/rs/xid v1.6.0 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/sagikazarmark/locafero v0.10.0 // indirect
github.com/sagikazarmark/locafero v0.12.0 // indirect
github.com/shopspring/decimal v1.4.0 // indirect
github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
github.com/slackhq/nebula v1.9.6 // indirect
github.com/smallstep/certificates v0.28.4 // indirect
github.com/smallstep/cli-utils v0.12.1 // indirect
github.com/smallstep/linkedca v0.23.0 // indirect
github.com/slackhq/nebula v1.9.7 // indirect
github.com/smallstep/certificates v0.29.0 // indirect
github.com/smallstep/cli-utils v0.12.2 // indirect
github.com/smallstep/linkedca v0.25.0 // indirect
github.com/smallstep/nosql v0.7.0 // indirect
github.com/smallstep/pkcs7 v0.2.1 // indirect
github.com/smallstep/scep v0.0.0-20250318231241-a25cabb69492 // indirect
github.com/smallstep/truststore v0.13.0 // indirect
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect
github.com/spf13/afero v1.14.0 // indirect
github.com/spf13/cast v1.9.2 // indirect
github.com/spf13/afero v1.15.0 // indirect
github.com/spf13/cast v1.10.0 // indirect
github.com/spf13/pflag v1.0.10 // indirect
github.com/spf13/viper v1.20.1 // indirect
github.com/spf13/viper v1.21.0 // indirect
github.com/stoewer/go-strcase v1.3.1 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
github.com/tailscale/tscert v0.0.0-20240608151842-d3f834017e53 // indirect
github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55 // indirect
github.com/tailscale/tscert v0.0.0-20251216020129-aea342f6d747 // indirect
github.com/tidwall/gjson v1.18.0 // indirect
github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/match v1.2.0 // indirect
github.com/tidwall/pretty v1.2.1 // indirect
github.com/tidwall/sjson v1.2.5 // indirect
github.com/unrolled/secure v1.17.0 // indirect
@@ -170,45 +167,45 @@ require (
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc // indirect
github.com/zeebo/blake3 v0.2.4 // indirect
go.etcd.io/bbolt v1.4.3 // indirect
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0 // indirect
go.opentelemetry.io/contrib/propagators/autoprop v0.62.0 // indirect
go.opentelemetry.io/contrib/propagators/aws v1.37.0 // indirect
go.opentelemetry.io/contrib/propagators/b3 v1.37.0 // indirect
go.opentelemetry.io/contrib/propagators/jaeger v1.37.0 // indirect
go.opentelemetry.io/contrib/propagators/ot v1.37.0 // indirect
go.opentelemetry.io/otel v1.37.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.37.0 // indirect
go.opentelemetry.io/otel/metric v1.37.0 // indirect
go.opentelemetry.io/otel/sdk v1.37.0 // indirect
go.opentelemetry.io/otel/trace v1.37.0 // indirect
go.opentelemetry.io/proto/otlp v1.7.1 // indirect
go.step.sm/crypto v0.70.0 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0 // indirect
go.opentelemetry.io/contrib/propagators/autoprop v0.64.0 // indirect
go.opentelemetry.io/contrib/propagators/aws v1.39.0 // indirect
go.opentelemetry.io/contrib/propagators/b3 v1.39.0 // indirect
go.opentelemetry.io/contrib/propagators/jaeger v1.39.0 // indirect
go.opentelemetry.io/contrib/propagators/ot v1.39.0 // indirect
go.opentelemetry.io/otel v1.39.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.39.0 // indirect
go.opentelemetry.io/otel/metric v1.39.0 // indirect
go.opentelemetry.io/otel/sdk v1.39.0 // indirect
go.opentelemetry.io/otel/trace v1.39.0 // indirect
go.opentelemetry.io/proto/otlp v1.9.0 // indirect
go.step.sm/crypto v0.75.0 // indirect
go.uber.org/automaxprocs v1.6.0 // indirect
go.uber.org/mock v0.6.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.27.0 // indirect
go.uber.org/zap v1.27.1 // indirect
go.uber.org/zap/exp v0.3.0 // indirect
go.yaml.in/yaml/v2 v2.4.2 // indirect
golang.org/x/crypto v0.42.0 // indirect
golang.org/x/crypto/x509roots/fallback v0.0.0-20250826074233-8f580defa01d // indirect
golang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b // indirect
golang.org/x/mod v0.28.0 // indirect
golang.org/x/net v0.44.0 // indirect
golang.org/x/oauth2 v0.30.0 // indirect
golang.org/x/sync v0.17.0 // indirect
golang.org/x/sys v0.36.0 // indirect
golang.org/x/term v0.35.0 // indirect
golang.org/x/text v0.29.0 // indirect
golang.org/x/time v0.12.0 // indirect
golang.org/x/tools v0.37.0 // indirect
google.golang.org/api v0.248.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20250826171959-ef028d996bc1 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250826171959-ef028d996bc1 // indirect
google.golang.org/grpc v1.75.0 // indirect
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1 // indirect
google.golang.org/protobuf v1.36.8 // indirect
go.yaml.in/yaml/v2 v2.4.3 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/crypto v0.46.0 // indirect
golang.org/x/crypto/x509roots/fallback v0.0.0-20251210140736-7dacc380ba00 // indirect
golang.org/x/exp v0.0.0-20251209150349-8475f28825e9 // indirect
golang.org/x/mod v0.31.0 // indirect
golang.org/x/net v0.48.0 // indirect
golang.org/x/oauth2 v0.34.0 // indirect
golang.org/x/sync v0.19.0 // indirect
golang.org/x/sys v0.39.0 // indirect
golang.org/x/term v0.38.0 // indirect
golang.org/x/text v0.32.0 // indirect
golang.org/x/time v0.14.0 // indirect
golang.org/x/tools v0.40.0 // indirect
google.golang.org/api v0.257.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20251213004720-97cd9d5aeac2 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2 // indirect
google.golang.org/grpc v1.77.0 // indirect
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.6.0 // indirect
google.golang.org/protobuf v1.36.11 // indirect
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
howett.net/plist v1.0.1 // indirect

View File

@@ -1,119 +1,101 @@
cel.dev/expr v0.24.0 h1:56OvJKSH3hDGL0ml5uSxZmz3/3Pq4tJ+fb1unVLAFcY=
cel.dev/expr v0.24.0/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw=
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.31.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.37.0/go.mod h1:TS1dMSSfndXH133OKGwekG838Om/cQT0BUHV3HcBgoo=
cloud.google.com/go v0.120.0 h1:wc6bgG9DHyKqF5/vQvX1CiZrtHnxJjBlKUyF9nP6meA=
cloud.google.com/go v0.120.0/go.mod h1:/beW32s8/pGRuj4IILWQNd4uuebeT4dkOhKmkfit64Q=
cloud.google.com/go/auth v0.16.5 h1:mFWNQ2FEVWAliEQWpAdH80omXFokmrnbDhUS9cBywsI=
cloud.google.com/go/auth v0.16.5/go.mod h1:utzRfHMP+Vv0mpOkTRQoWD2q3BatTOoWbA7gCc2dUhQ=
cel.dev/expr v0.25.1 h1:1KrZg61W6TWSxuNZ37Xy49ps13NUovb66QLprthtwi4=
cel.dev/expr v0.25.1/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4=
cloud.google.com/go v0.121.6 h1:waZiuajrI28iAf40cWgycWNgaXPO06dupuS+sgibK6c=
cloud.google.com/go v0.121.6/go.mod h1:coChdst4Ea5vUpiALcYKXEpR1S9ZgXbhEzzMcMR66vI=
cloud.google.com/go/auth v0.18.0 h1:wnqy5hrv7p3k7cShwAU/Br3nzod7fxoqG+k0VZ+/Pk0=
cloud.google.com/go/auth v0.18.0/go.mod h1:wwkPM1AgE1f2u6dG443MiWoD8C3BtOywNsUMcUTVDRo=
cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc=
cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=
cloud.google.com/go/compute/metadata v0.8.0 h1:HxMRIbao8w17ZX6wBnjhcDkW6lTFpgcaobyVfZWqRLA=
cloud.google.com/go/compute/metadata v0.8.0/go.mod h1:sYOGTp851OV9bOFJ9CH7elVvyzopvWQFNNghtDQ/Biw=
cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs=
cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10=
cloud.google.com/go/iam v1.5.2 h1:qgFRAGEmd8z6dJ/qyEchAuL9jpswyODjA2lS+w234g8=
cloud.google.com/go/iam v1.5.2/go.mod h1:SE1vg0N81zQqLzQEwxL2WI6yhetBdbNQuTvIKCSkUHE=
cloud.google.com/go/kms v1.22.0 h1:dBRIj7+GDeeEvatJeTB19oYZNV0aj6wEqSIT/7gLqtk=
cloud.google.com/go/kms v1.22.0/go.mod h1:U7mf8Sva5jpOb4bxYZdtw/9zsbIjrklYwPcvMk34AL8=
cloud.google.com/go/longrunning v0.6.7 h1:IGtfDWHhQCgCjwQjV9iiLnUta9LBCo8R9QmAFsS/PrE=
cloud.google.com/go/longrunning v0.6.7/go.mod h1:EAFV3IZAKmM56TyiE6VAP3VoTzhZzySwI/YI1s/nRsY=
cloud.google.com/go/kms v1.23.2 h1:4IYDQL5hG4L+HzJBhzejUySoUOheh3Lk5YT4PCyyW6k=
cloud.google.com/go/kms v1.23.2/go.mod h1:rZ5kK0I7Kn9W4erhYVoIRPtpizjunlrfU4fUkumUp8g=
cloud.google.com/go/longrunning v0.7.0 h1:FV0+SYF1RIj59gyoWDRi45GiYUMM3K1qO51qoboQT1E=
cloud.google.com/go/longrunning v0.7.0/go.mod h1:ySn2yXmjbK9Ba0zsQqunhDkYi0+9rlXIwnoAf+h+TPY=
dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
dmitri.shuralyov.com/app/changes v0.0.0-20180602232624-0a106ad413e3/go.mod h1:Yl+fi1br7+Rr3LqpNJf1/uxUdtRUV+Tnj0o93V2B9MU=
dmitri.shuralyov.com/html/belt v0.0.0-20180602232347-f7d459c86be0/go.mod h1:JLBrvjyP0v+ecvNYvCpyZgu5/xkfAUhi6wJj28eUfSU=
dmitri.shuralyov.com/service/change v0.0.0-20181023043359-a85b471d5412/go.mod h1:a1inKt/atXimZ4Mv927x+r7UpyzRUf4emIoiiSC2TN4=
dmitri.shuralyov.com/state v0.0.0-20180228185332-28bcc343414c/go.mod h1:0PRwlb0D6DFvNNtx+9ybjezNCa8XF0xaYcETyp6rHWU=
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
git.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg=
github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96 h1:cTp8I5+VIoKjsnZuH8vjyaysT/ses3EvZeaV/1UkF2M=
github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96/go.mod h1:bOvUY6CB00SOBii9/FifXqc0awNKxLFCL/+pkDPuyl8=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg=
github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/KimMachineGun/automemlimit v0.7.4 h1:UY7QYOIfrr3wjjOAqahFmC3IaQCLWvur9nmfIn6LnWk=
github.com/KimMachineGun/automemlimit v0.7.4/go.mod h1:QZxpHaGOQoYvFhv/r4u3U0JTC2ZcOwbSr11UZF46UBM=
github.com/KimMachineGun/automemlimit v0.7.5 h1:RkbaC0MwhjL1ZuBKunGDjE/ggwAX43DwZrJqVwyveTk=
github.com/KimMachineGun/automemlimit v0.7.5/go.mod h1:QZxpHaGOQoYvFhv/r4u3U0JTC2ZcOwbSr11UZF46UBM=
github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=
github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0=
github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
github.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe3tPhs=
github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0=
github.com/MauriceGit/skiplist v0.0.0-20191117202105-643e379adb62/go.mod h1:877WBceefKn14QwVVn4xRFUsHsZb9clICgdeTj4XsUg=
github.com/MauriceGit/skiplist v0.0.0-20211105230623-77f5c8d3e145 h1:1yw6O62BReQ+uA1oyk9XaQTvLhcoHWmoQAgXmDFXpIY=
github.com/MauriceGit/skiplist v0.0.0-20211105230623-77f5c8d3e145/go.mod h1:877WBceefKn14QwVVn4xRFUsHsZb9clICgdeTj4XsUg=
github.com/MicahParks/jwkset v0.9.6 h1:Tf8l2/MOby5Kh3IkrqzThPQKfLytMERoAsGZKlyYZxg=
github.com/MicahParks/jwkset v0.9.6/go.mod h1:U2oRhRaLgDCLjtpGL2GseNKGmZtLs/3O7p+OZaL5vo0=
github.com/MicahParks/keyfunc/v3 v3.6.1 h1:A8A5zGZ8XmRyxizSY7s5FLY/aSplrnEBLCOrC0D1ojM=
github.com/MicahParks/keyfunc/v3 v3.6.1/go.mod h1:y6Ed3dMgNKTcpxbaQHD8mmrYDUZWJAxteddA6OQj+ag=
github.com/Microsoft/go-winio v0.6.0/go.mod h1:cTAf44im0RAYeL23bpB+fzCyDH2MJiz2BO69KH/soAE=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/MicahParks/jwkset v0.11.0 h1:yc0zG+jCvZpWgFDFmvs8/8jqqVBG9oyIbmBtmjOhoyQ=
github.com/MicahParks/jwkset v0.11.0/go.mod h1:U2oRhRaLgDCLjtpGL2GseNKGmZtLs/3O7p+OZaL5vo0=
github.com/MicahParks/keyfunc/v3 v3.7.0 h1:pdafUNyq+p3ZlvjJX1HWFP7MA3+cLpDtg69U3kITJGM=
github.com/MicahParks/keyfunc/v3 v3.7.0/go.mod h1:z66bkCviwqfg2YUp+Jcc/xRE9IXLcMq6DrgV/+Htru0=
github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE=
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
github.com/RoaringBitmap/roaring v0.9.4/go.mod h1:icnadbWcNyfEHlYdr+tDlOTih1Bf/h+rzPpv4sbomAA=
github.com/RoaringBitmap/roaring v1.9.4 h1:yhEIoH4YezLYT04s1nHehNO64EKFTop/wBhxv2QzDdQ=
github.com/RoaringBitmap/roaring v1.9.4/go.mod h1:6AXUsoIEzDTFFQCe1RbGA6uFONMhvejWj5rqITANK90=
github.com/RoaringBitmap/roaring/v2 v2.14.4 h1:4aKySrrg9G/5oRtJ3TrZLObVqxgQ9f1znCRBwEwjuVw=
github.com/RoaringBitmap/roaring/v2 v2.14.4/go.mod h1:oMvV6omPWr+2ifRdeZvVJyaz+aoEUopyv5iH0u/+wbY=
github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0=
github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
github.com/alecthomas/chroma/v2 v2.2.0/go.mod h1:vf4zrexSH54oEjJ7EdB65tGNHmH3pGZmVkgTP5RHvAs=
github.com/alecthomas/chroma/v2 v2.20.0 h1:sfIHpxPyR07/Oylvmcai3X/exDlE8+FA820NTz+9sGw=
github.com/alecthomas/chroma/v2 v2.20.0/go.mod h1:e7tViK0xh/Nf4BYHl00ycY6rV7b8iXBksI9E359yNmA=
github.com/alecthomas/chroma/v2 v2.21.0 h1:YVW9qQAFnQm2OFPPFQg6G/TpMxKSsUr/KUPDi/BEqtY=
github.com/alecthomas/chroma/v2 v2.21.0/go.mod h1:NqVhfBR0lte5Ouh3DcthuUCTUpDC9cxBOfyMbMQPs3o=
github.com/alecthomas/repr v0.0.0-20220113201626-b1b626ac65ae/go.mod h1:2kn6fqh/zIyPLmm3ugklbEi5hg5wS435eygvNfaDQL8=
github.com/alecthomas/repr v0.5.1 h1:E3G4t2QbHTSNpPKBgMTln5KLkZHLOcU7r37J4pXBuIg=
github.com/alecthomas/repr v0.5.1/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c=
github.com/alecthomas/repr v0.5.2 h1:SU73FTI9D1P5UNtvseffFSGmdNci/O6RsqzeXJtP0Qs=
github.com/alecthomas/repr v0.5.2/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ=
github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw=
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
github.com/aryann/difflib v0.0.0-20210328193216-ff5ff6dc229b h1:uUXgbcPDK3KpW29o4iy7GtuappbWT0l5NaMo9H9pJDw=
github.com/aryann/difflib v0.0.0-20210328193216-ff5ff6dc229b/go.mod h1:DAHtR1m6lCRdSC2Tm3DSWRPvIPr6xNKyeHdqDQSQT+A=
github.com/aws/aws-sdk-go-v2 v1.38.0 h1:UCRQ5mlqcFk9HJDIqENSLR3wiG1VTWlyUfLDEvY7RxU=
github.com/aws/aws-sdk-go-v2 v1.38.0/go.mod h1:9Q0OoGQoboYIAJyslFyF1f5K1Ryddop8gqMhWx/n4Wg=
github.com/aws/aws-sdk-go-v2/config v1.31.0 h1:9yH0xiY5fUnVNLRWO0AtayqwU1ndriZdN78LlhruJR4=
github.com/aws/aws-sdk-go-v2/config v1.31.0/go.mod h1:VeV3K72nXnhbe4EuxxhzsDc/ByrCSlZwUnWH52Nde/I=
github.com/aws/aws-sdk-go-v2/credentials v1.18.4 h1:IPd0Algf1b+Qy9BcDp0sCUcIWdCQPSzDoMK3a8pcbUM=
github.com/aws/aws-sdk-go-v2/credentials v1.18.4/go.mod h1:nwg78FjH2qvsRM1EVZlX9WuGUJOL5od+0qvm0adEzHk=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.3 h1:GicIdnekoJsjq9wqnvyi2elW6CGMSYKhdozE7/Svh78=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.3/go.mod h1:R7BIi6WNC5mc1kfRM7XM/VHC3uRWkjc396sfabq4iOo=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.3 h1:o9RnO+YZ4X+kt5Z7Nvcishlz0nksIt2PIzDglLMP0vA=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.3/go.mod h1:+6aLJzOG1fvMOyzIySYjOFjcguGvVRL68R+uoRencN4=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.3 h1:joyyUFhiTQQmVK6ImzNU9TQSNRNeD9kOklqTzyk5v6s=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.3/go.mod h1:+vNIyZQP3b3B1tSLI0lxvrU9cfM7gpdRXMFfm67ZcPc=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 h1:bIqFDwgGXXN1Kpp99pDOdKMTTb5d2KyU5X/BZxjOkRo=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3/go.mod h1:H5O/EsxDWyU+LP/V8i5sm8cxoZgc2fdNR9bxlOFrQTo=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.0 h1:6+lZi2JeGKtCraAj1rpoZfKqnQ9SptseRZioejfUOLM=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.0/go.mod h1:eb3gfbVIxIoGgJsi9pGne19dhCBpK6opTYpQqAmdy44=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.3 h1:ieRzyHXypu5ByllM7Sp4hC5f/1Fy5wqxqY0yB85hC7s=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.3/go.mod h1:O5ROz8jHiOAKAwx179v+7sHMhfobFVi6nZt8DEyiYoM=
github.com/aws/aws-sdk-go-v2/service/kms v1.44.0 h1:Z95XCqqSnwXr0AY7PgsiOUBhUG2GoDM5getw6RfD1Lg=
github.com/aws/aws-sdk-go-v2/service/kms v1.44.0/go.mod h1:DqcSngL7jJeU1fOzh5Ll5rSvX/MlMV6OZlE4mVdFAQc=
github.com/aws/aws-sdk-go-v2/service/sso v1.28.0 h1:Mc/MKBf2m4VynyJkABoVEN+QzkfLqGj0aiJuEe7cMeM=
github.com/aws/aws-sdk-go-v2/service/sso v1.28.0/go.mod h1:iS5OmxEcN4QIPXARGhavH7S8kETNL11kym6jhoS7IUQ=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.33.0 h1:6csaS/aJmqZQbKhi1EyEMM7yBW653Wy/B9hnBofW+sw=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.33.0/go.mod h1:59qHWaY5B+Rs7HGTuVGaC32m0rdpQ68N8QCN3khYiqs=
github.com/aws/aws-sdk-go-v2/service/sts v1.37.0 h1:MG9VFW43M4A8BYeAfaJJZWrroinxeTi2r3+SnmLQfSA=
github.com/aws/aws-sdk-go-v2/service/sts v1.37.0/go.mod h1:JdeBDPgpJfuS6rU/hNglmOigKhyEZtBmbraLE4GK1J8=
github.com/aws/smithy-go v1.22.5 h1:P9ATCXPMb2mPjYBgueqJNCA5S9UfktsW0tTxi+a7eqw=
github.com/aws/smithy-go v1.22.5/go.mod h1:t1ufH5HMublsJYulve2RKmHDC15xu1f26kHCp/HgceI=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/aws/aws-sdk-go-v2 v1.40.0 h1:/WMUA0kjhZExjOQN2z3oLALDREea1A7TobfuiBrKlwc=
github.com/aws/aws-sdk-go-v2 v1.40.0/go.mod h1:c9pm7VwuW0UPxAEYGyTmyurVcNrbF6Rt/wixFqDhcjE=
github.com/aws/aws-sdk-go-v2/config v1.32.1 h1:iODUDLgk3q8/flEC7ymhmxjfoAnBDwEEYEVyKZ9mzjU=
github.com/aws/aws-sdk-go-v2/config v1.32.1/go.mod h1:xoAgo17AGrPpJBSLg81W+ikM0cpOZG8ad04T2r+d5P0=
github.com/aws/aws-sdk-go-v2/credentials v1.19.1 h1:JeW+EwmtTE0yXFK8SmklrFh/cGTTXsQJumgMZNlbxfM=
github.com/aws/aws-sdk-go-v2/credentials v1.19.1/go.mod h1:BOoXiStwTF+fT2XufhO0Efssbi1CNIO/ZXpZu87N0pw=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.14 h1:WZVR5DbDgxzA0BJeudId89Kmgy6DIU4ORpxwsVHz0qA=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.14/go.mod h1:Dadl9QO0kHgbrH1GRqGiZdYtW5w+IXXaBNCHTIaheM4=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.14 h1:PZHqQACxYb8mYgms4RZbhZG0a7dPW06xOjmaH0EJC/I=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.14/go.mod h1:VymhrMJUWs69D8u0/lZ7jSB6WgaG/NqHi3gX0aYf6U0=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.14 h1:bOS19y6zlJwagBfHxs0ESzr1XCOU2KXJCWcq3E2vfjY=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.14/go.mod h1:1ipeGBMAxZ0xcTm6y6paC2C/J6f6OO7LBODV9afuAyM=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 h1:WKuaxf++XKWlHWu9ECbMlha8WOEGm0OUEZqm4K/Gcfk=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4/go.mod h1:ZWy7j6v1vWGmPReu0iSGvRiise4YI5SkR3OHKTZ6Wuc=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.3 h1:x2Ibm/Af8Fi+BH+Hsn9TXGdT+hKbDd5XOTZxTMxDk7o=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.3/go.mod h1:IW1jwyrQgMdhisceG8fQLmQIydcT/jWY21rFhzgaKwo=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.14 h1:FIouAnCE46kyYqyhs0XEBDFFSREtdnr8HQuLPQPLCrY=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.14/go.mod h1:UTwDc5COa5+guonQU8qBikJo1ZJ4ln2r1MkF7Dqag1E=
github.com/aws/aws-sdk-go-v2/service/kms v1.48.0 h1:pQgVxqqNOacqb19+xaoih/wNLil4d8tgi+FxtBi/qQY=
github.com/aws/aws-sdk-go-v2/service/kms v1.48.0/go.mod h1:VJcNH6BLr+3VJwinRKdotLOMglHO8mIKlD3ea5c7hbw=
github.com/aws/aws-sdk-go-v2/service/signin v1.0.1 h1:BDgIUYGEo5TkayOWv/oBLPphWwNm/A91AebUjAu5L5g=
github.com/aws/aws-sdk-go-v2/service/signin v1.0.1/go.mod h1:iS6EPmNeqCsGo+xQmXv0jIMjyYtQfnwg36zl2FwEouk=
github.com/aws/aws-sdk-go-v2/service/sso v1.30.4 h1:U//SlnkE1wOQiIImxzdY5PXat4Wq+8rlfVEw4Y7J8as=
github.com/aws/aws-sdk-go-v2/service/sso v1.30.4/go.mod h1:av+ArJpoYf3pgyrj6tcehSFW+y9/QvAY8kMooR9bZCw=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.9 h1:LU8S9W/mPDAU9q0FjCLi0TrCheLMGwzbRpvUMwYspcA=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.9/go.mod h1:/j67Z5XBVDx8nZVp9EuFM9/BS5dvBznbqILGuu73hug=
github.com/aws/aws-sdk-go-v2/service/sts v1.41.1 h1:GdGmKtG+/Krag7VfyOXV17xjTCz0i9NT+JnqLTOI5nA=
github.com/aws/aws-sdk-go-v2/service/sts v1.41.1/go.mod h1:6TxbXoDSgBQ225Qd8Q+MbxUxUh6TtNKwbRt/EPS9xso=
github.com/aws/smithy-go v1.23.2 h1:Crv0eatJUQhaManss33hS5r40CG3ZFH+21XSkqMrIUM=
github.com/aws/smithy-go v1.23.2/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bits-and-blooms/bitset v1.2.0/go.mod h1:gIdJ4wp64HaoK2YrL1Q5/N7Y16edYb8uY+O0FJTyyDA=
github.com/bits-and-blooms/bitset v1.12.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
github.com/bits-and-blooms/bitset v1.24.0 h1:H4x4TuulnokZKvHLfzVRTHJfFfnHEeSYJizujEZvmAM=
github.com/bits-and-blooms/bitset v1.24.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
github.com/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625/go.mod h1:HYsPBTaaSFSlLx/70C2HPIMNZpVV8+vt/A+FMnYP11g=
github.com/buger/jsonparser v0.0.0-20181115193947-bf1c66bbce23/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s=
github.com/bits-and-blooms/bitset v1.24.4 h1:95H15Og1clikBrKr/DuzMXkQzECs1M6hhoGXLwLQOZE=
github.com/bits-and-blooms/bitset v1.24.4/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
github.com/caddyserver/caddy/v2 v2.10.2 h1:g/gTYjGMD0dec+UgMw8SnfmJ3I9+M2TdvoRL/Ovu6U8=
github.com/caddyserver/caddy/v2 v2.10.2/go.mod h1:TXLQHx+ev4HDpkO6PnVVHUbL6OXt6Dfe7VcIBdQnPL0=
github.com/caddyserver/certmagic v0.25.0 h1:VMleO/XA48gEWes5l+Fh6tRWo9bHkhwAEhx63i+F5ic=
github.com/caddyserver/certmagic v0.25.0/go.mod h1:m9yB7Mud24OQbPHOiipAoyKPn9pKHhpSJxXR1jydBxA=
github.com/caddyserver/zerossl v0.1.3 h1:onS+pxp3M8HnHpN5MMbOMyNjmTheJyWRaZYwn+YTAyA=
github.com/caddyserver/zerossl v0.1.3/go.mod h1:CxA0acn7oEGO6//4rtrRjYgEoa4MFw/XofZnrYwGqG4=
github.com/ccoveille/go-safecast v1.6.1 h1:Nb9WMDR8PqhnKCVs2sCB+OqhohwO5qaXtCviZkIff5Q=
github.com/ccoveille/go-safecast v1.6.1/go.mod h1:QqwNjxQ7DAqY0C721OIO9InMk9zCwcsO7tnRuHytad8=
github.com/ccoveille/go-safecast/v2 v2.0.0 h1:+5eyITXAUj3wMjad6cRVJKGnC7vDS55zk0INzJagub0=
github.com/ccoveille/go-safecast/v2 v2.0.0/go.mod h1:JIYA4CAR33blIDuE6fSwCp2sz1oOBahXnvmdBhOAABs=
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko=
@@ -129,15 +111,13 @@ github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObk
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/chzyer/test v1.0.0 h1:p3BQDXSxOhOG0P9z6/hGnII4LGiEPOYBhs8asl/fC04=
github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0=
github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
github.com/coreos/go-oidc/v3 v3.15.0 h1:R6Oz8Z4bqWR7VFQ+sPSvZPQv4x8M+sJkDO5ojgwlyAg=
github.com/coreos/go-oidc/v3 v3.15.0/go.mod h1:HaZ3szPaZ0e4r6ebqvsLWlk2Tn+aejfmrfah6hnSYEU=
github.com/coreos/go-oidc/v3 v3.17.0 h1:hWBGaQfbi0iVviX4ibC7bk8OKT5qNr4klBaCHVNvehc=
github.com/coreos/go-oidc/v3 v3.17.0/go.mod h1:wqPbKFrVnE90vty060SB40FCJ8fTHTxSwyXJqZH+sI8=
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/coreos/go-systemd v0.0.0-20181012123002-c6f51f82210d/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo=
@@ -161,16 +141,16 @@ github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55k
github.com/dlclark/regexp2 v1.7.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ=
github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/dolthub/maphash v0.1.0 h1:bsQ7JsF4FkkWyrP3oCnFJgrCUAFbFf3kOl4L/QxPDyQ=
github.com/dolthub/maphash v0.1.0/go.mod h1:gkg4Ch4CdCDu5h6PMriVLawB7koZ+5ijb9puGMV50a4=
github.com/dunglas/caddy-cbrotli v1.0.1 h1:mkg7EB1GmoyfBt3kY3mq4o/0bfnBeq7ZLQjmVmdBE3Y=
github.com/dunglas/caddy-cbrotli v1.0.1/go.mod h1:uXABy3tjy1FABF+3JWKVh1ajFvIO/kfpwHaeZGSBaAY=
github.com/dunglas/httpsfv v1.1.0 h1:Jw76nAyKWKZKFrpMMcL76y35tOpYHqQPzHQiwDvpe54=
github.com/dunglas/httpsfv v1.1.0/go.mod h1:zID2mqw9mFsnt7YC3vYQ9/cjq30q41W+1AnDwH8TiMg=
github.com/dunglas/mercure v0.20.2 h1:zQRJ7QgjtQafOc9q3XtYVDgsfbUokvHeBhiFBpNTiIs=
github.com/dunglas/mercure v0.20.2/go.mod h1:sJtQEtezUTKlbDCK1Sqgt9sUrcwBNZJxVc6oHkqvt3w=
github.com/dunglas/mercure/caddy v0.20.2 h1:CGpcT+arSzxr39s2qRvPpS2/yjZV26cElgee+9Au7lg=
github.com/dunglas/mercure/caddy v0.20.2/go.mod h1:96MOkU7u42rFRIQSpzQibC2LT1vwo/AgLA5jivuw7aE=
github.com/dunglas/mercure v0.21.4 h1:mXPXHfB+4cYfFFCRRDY198mfef5+MQcdCpUnAHBUW2M=
github.com/dunglas/mercure v0.21.4/go.mod h1:l/dglCjp/OQx8/quRyceRPx2hqZQ3CNviwoLMRQiJ/k=
github.com/dunglas/mercure/caddy v0.21.4 h1:7o+6rDqfwj1EOmXOgfBFsayvJvOUP37xujQHaxuX4ps=
github.com/dunglas/mercure/caddy v0.21.4/go.mod h1:EM2s+OVGExbSXObUdAPDwPsbQw4t/icLtQv9CFylDvY=
github.com/dunglas/skipfilter v1.0.0 h1:JG9SgGg4n6BlFwuTYzb9RIqjH7PfwszvWehanrYWPF4=
github.com/dunglas/skipfilter v1.0.0/go.mod h1:ryhr8j7CAHSjzeN7wI6YEuwoArQ3OQmRqWWVCEAfb9w=
github.com/dunglas/vulcain v1.2.1 h1:pkPwvIfoa/xmWSVUyhntbIKT+XO2VFMyhLKv1gA61O8=
github.com/dunglas/vulcain v1.2.1/go.mod h1:Qv1hxHP8rMDp6ZrpQQPSOh1OibuAYScNNCL/46sCRXU=
github.com/dunglas/vulcain/caddy v1.2.1 h1:Bh5awZ9WoNqsBv7OZfs1SktJqRgJaF5avI5oDmxs6lI=
@@ -178,11 +158,10 @@ github.com/dunglas/vulcain/caddy v1.2.1/go.mod h1:8QrmLTfURmW2VgjTR6Gb9a53FrZjsp
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/e-dant/watcher/watcher-go v0.0.0-20251208164151-f88ec3b7e146 h1:h3vVM6X45PK0mAk8NqiYNQGXTyhvXy1HQ5GhuQN4eeA=
github.com/e-dant/watcher/watcher-go v0.0.0-20251208164151-f88ec3b7e146/go.mod h1:sVUOkwtftoj71nnJRG2S0oWNfXFdKpz/M9vK0z06nmM=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
github.com/francoispqt/gojay v1.2.13 h1:d2m3sFjloqoIUQU3TsHBgj6qg/BVGlTBeHDUmyJnXKk=
github.com/francoispqt/gojay v1.2.13/go.mod h1:ehT5mTG4ua4581f1++1WLG0vPdaA9HaiDsoyrBGkyDY=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
@@ -190,28 +169,25 @@ github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM=
github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
github.com/gammazero/deque v1.1.0 h1:OyiyReBbnEG2PP0Bnv1AASLIYvyKqIFN5xfl1t8oGLo=
github.com/gammazero/deque v1.1.0/go.mod h1:JVrR+Bj1NMQbPnYclvDlvSX0nVGReLrQZ0aUMuWLctg=
github.com/getkin/kin-openapi v0.133.0 h1:pJdmNohVIJ97r4AUFtEXRXwESr8b0bD721u/Tz6k8PQ=
github.com/getkin/kin-openapi v0.133.0/go.mod h1:boAciF6cXk5FhPqe/NQeBTeenbjqU4LhWBf09ILVvWE=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/gliderlabs/ssh v0.1.1/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=
github.com/go-chi/chi/v5 v5.2.3 h1:WQIt9uxdsAbgIYgid+BpYc+liqQZGMHRaUwp0JUcvdE=
github.com/go-chi/chi/v5 v5.2.3/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
github.com/go-jose/go-jose/v3 v3.0.4 h1:Wp5HA7bLQcKnf6YYao/4kpRpVMp/yf6+pJKV8WFSaNY=
github.com/go-jose/go-jose/v3 v3.0.4/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ=
github.com/go-jose/go-jose/v4 v4.1.2 h1:TK/7NqRQZfgAh+Td8AlsrvtPoUyiHh0LqVvokh+1vHI=
github.com/go-jose/go-jose/v4 v4.1.2/go.mod h1:22cg9HWM1pOlnRiY+9cQYJ9XHmya1bYW8OeDM6Ku6Oo=
github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs=
github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-openapi/jsonpointer v0.21.2 h1:AqQaNADVwq/VnkCmQg6ogE+M3FOsKTytwges0JdwVuA=
github.com/go-openapi/jsonpointer v0.21.2/go.mod h1:50I1STOfbY1ycR8jGz8DaMeLCdXiI6aDteEdRNNzpdk=
github.com/go-openapi/swag v0.23.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZU=
github.com/go-openapi/swag v0.23.1/go.mod h1:STZs8TbRvEQQKUA+JZNAm3EWlgaOBGpyFDqQnDHMef0=
github.com/go-openapi/jsonpointer v0.22.4 h1:dZtK82WlNpVLDW2jlA1YCiVJFVqkED1MegOUy9kR5T4=
github.com/go-openapi/jsonpointer v0.22.4/go.mod h1:elX9+UgznpFhgBuaMQ7iu4lvvX1nvNsesQ3oxmYTw80=
github.com/go-openapi/swag/jsonname v0.25.4 h1:bZH0+MsS03MbnwBXYhuTttMOqk+5KcQ9869Vye1bNHI=
github.com/go-openapi/swag/jsonname v0.25.4/go.mod h1:GPVEk9CWVhNvWhZgrnvRA6utbAltopbKwDu8mXNUMag=
github.com/go-openapi/testify/v2 v2.0.2 h1:X999g3jeLcoY8qctY/c/Z8iBHTbwLz7R2WXd6Ub6wls=
github.com/go-openapi/testify/v2 v2.0.2/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54=
github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo=
github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM=
@@ -220,16 +196,10 @@ github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9L
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA=
github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/gofrs/uuid/v5 v5.3.2 h1:2jfO8j3XgSwlz/wHqemAEugfnTlikAYHhnqQ8Xh4fE0=
github.com/gofrs/uuid/v5 v5.3.2/go.mod h1:CDOjlDMVAtN56jqyRUZh58JT31Tiw7/oQyEXZV+9bD8=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gofrs/uuid/v5 v5.4.0 h1:EfbpCTjqMuGyq5ZJwxqzn3Cbr2d0rUZU7v5ycAk/e/0=
github.com/gofrs/uuid/v5 v5.4.0/go.mod h1:CDOjlDMVAtN56jqyRUZh58JT31Tiw7/oQyEXZV+9bD8=
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
@@ -238,7 +208,6 @@ github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs=
github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/brotli/go/cbrotli v1.1.0 h1:YwHD/rwSgUSL4b2S3ZM2jnNymm+tmwKQqjUIC63nmHU=
github.com/google/brotli/go/cbrotli v1.1.0/go.mod h1:nOPhAkwVliJdNTkj3gXpljmWhjc4wCaVqbMJcPKWP4s=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg=
github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
github.com/google/cel-go v0.26.1 h1:iPbVVEdkhTX++hpe3lzSk7D3G3QSYqLGoHOcEio+UXQ=
@@ -246,43 +215,30 @@ github.com/google/cel-go v0.26.1/go.mod h1:A9O8OU9rdvrK5MQyrqfIxo1a0u4g3sF8KB6PU
github.com/google/certificate-transparency-go v1.0.21/go.mod h1:QeJfpSbVSfYc7RgB3gJFj9cbuQMMchQxrWXz8Ruopmg=
github.com/google/certificate-transparency-go v1.3.2 h1:9ahSNZF2o7SYMaKaXhAumVEzXB2QaayzII9C8rv7v+A=
github.com/google/certificate-transparency-go v1.3.2/go.mod h1:H5FpMUaGa5Ab2+KCYsxg6sELw3Flkl7pGZzWdBoYLXs=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ=
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
github.com/google/go-tpm v0.9.5 h1:ocUmnDebX54dnW+MQWGQRbdaAcJELsa6PqZhJ48KwVU=
github.com/google/go-tpm v0.9.5/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY=
github.com/google/go-tpm-tools v0.4.5 h1:3fhthtyMDbIZFR5/0y1hvUoZ1Kf4i1eZ7C73R4Pvd+k=
github.com/google/go-tpm-tools v0.4.5/go.mod h1:ktjTNq8yZFD6TzdBFefUfen96rF3NpYwpSb2d8bc+Y8=
github.com/google/go-tpm v0.9.7 h1:u89J4tUUeDTlH8xxC3CTW7OHZjbjKoHdQ9W7gCUhtxA=
github.com/google/go-tpm v0.9.7/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY=
github.com/google/go-tpm-tools v0.4.7 h1:J3ycC8umYxM9A4eF73EofRZu4BxY0jjQnUnkhIBbvws=
github.com/google/go-tpm-tools v0.4.7/go.mod h1:gSyXTZHe3fgbzb6WEGd90QucmsnT1SRdlye82gH8QjQ=
github.com/google/go-tspi v0.3.0 h1:ADtq8RKfP+jrTyIWIZDIYcKOMecRqNJFOew2IT0Inus=
github.com/google/go-tspi v0.3.0/go.mod h1:xfMGI3G0PhxCdNVcYr1C4C+EizojDg/TXuX5by8CiHI=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0=
github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/enterprise-certificate-proxy v0.3.6 h1:GW/XbdyBFQ8Qe+YAmFU9uHLo7OnF5tL52HFAgMmyrf4=
github.com/googleapis/enterprise-certificate-proxy v0.3.6/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA=
github.com/googleapis/gax-go v2.0.0+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY=
github.com/googleapis/gax-go/v2 v2.0.3/go.mod h1:LLvjysVCY1JZeum8Z6l8qUty8fiNwE08qbEPm1M08qg=
github.com/googleapis/enterprise-certificate-proxy v0.3.7 h1:zrn2Ee/nWmHulBx5sAVrGgAa0f2/R35S4DJwfFaUPFQ=
github.com/googleapis/enterprise-certificate-proxy v0.3.7/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA=
github.com/googleapis/gax-go/v2 v2.15.0 h1:SyjDc1mGgZU5LncH8gimWo9lW1DtIfPibOG81vgd/bo=
github.com/googleapis/gax-go/v2 v2.15.0/go.mod h1:zVVkkxAQHa1RQpg9z2AUCMnKhi0Qld9rcmyfL1OZhoc=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gorilla/handlers v1.5.2 h1:cLTUSsNkgcwhgRqvCNmdbRWG0A3N4F+M2nWKdScwyEE=
github.com/gorilla/handlers v1.5.2/go.mod h1:dX+xVpaxdSw+q0Qek8SSsl3dfMk3jNddUkMzo0GtH0w=
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
github.com/grpc-ecosystem/grpc-gateway v1.5.0/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 h1:8Tjv8EJ+pM1xP8mK6egEbD1OgnVTyacbefKhmbLhIhU=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2/go.mod h1:pkJQ2tZHJ0aFOVEEot6oZmaVEZcRme73eIFmhiVuRWs=
github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
github.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iPY6p1c=
github.com/hashicorp/golang-lru v1.0.2/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 h1:NmZ1PKzSTQbuGHw9DGPFomqkkLWMC+vZCkfs+FHv1Vg=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3/go.mod h1:zQrxl1YP88HQlA6i9c63DSVPFklWpGX4OWAc9bFuaH4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
@@ -295,22 +251,16 @@ github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsI
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx/v5 v5.7.5 h1:JHGfMnQY+IEtGM63d+NGMjoRpysB2JBwDr5fsngwmJs=
github.com/jackc/pgx/v5 v5.7.5/go.mod h1:aruU7o91Tc2q2cFp5h4uP3f6ztExVpyVv88Xl/8Vl8M=
github.com/jackc/pgx/v5 v5.7.6 h1:rWQc5FwZSPX58r1OQmkuaNicxdmExaEz5A2DO2hUuTk=
github.com/jackc/pgx/v5 v5.7.6/go.mod h1:aruU7o91Tc2q2cFp5h4uP3f6ztExVpyVv88Xl/8Vl8M=
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/jellevandenhooff/dkim v0.0.0-20150330215556-f50fe3d243e1/go.mod h1:E0B/fFc00Y+Rasa88328GlI/XbtyysCtTHZS8h7IrBU=
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/kevburnsjr/skipfilter v0.0.1 h1:EWl1lWUJfIehrKYIEkps0Cl67lCfS2pUM9iZFNajp7g=
github.com/kevburnsjr/skipfilter v0.0.1/go.mod h1:jfaRyFOYVUtIa6IIC+0mB1qiZqhHw+DKvFowCBuijSk=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.12.3/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk=
github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
@@ -318,7 +268,6 @@ github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfn
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/pty v1.1.3/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
@@ -326,27 +275,23 @@ github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/libdns/libdns v1.1.1 h1:wPrHrXILoSHKWJKGd0EiAVmiJbFShguILTg9leS/P/U=
github.com/libdns/libdns v1.1.1/go.mod h1:4Bj9+5CQiNMVGf87wjX4CY3HQJypUHRuLvlsfsZqLWQ=
github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI=
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4=
github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=
github.com/mailru/easyjson v0.9.1 h1:LbtsOm5WAswyWbvTEOqhypdPeZzHavpZx96/n553mR8=
github.com/mailru/easyjson v0.9.1/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=
github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA=
github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/maypok86/otter v1.2.4 h1:HhW1Pq6VdJkmWwcZZq19BlEQkHtI8xgsQzBVXJU0nfc=
github.com/maypok86/otter v1.2.4/go.mod h1:mKLfoI7v1HOmQMwFgX4QkRk23mX6ge3RDvjdHOWG4R4=
github.com/maypok86/otter/v2 v2.2.1 h1:hnGssisMFkdisYcvQ8L019zpYQcdtPse+g0ps2i7cfI=
github.com/maypok86/otter/v2 v2.2.1/go.mod h1:1NKY9bY+kB5jwCXBJfE59u+zAwOt6C7ni1FTlFFMqVs=
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI=
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
github.com/mholt/acmez/v3 v3.1.3 h1:gUl789rjbJSuM5hYzOFnNaGgWPV1xVfnOs59o0dZEcc=
github.com/mholt/acmez/v3 v3.1.3/go.mod h1:L1wOU06KKvq7tswuMDwKdcHeKpFFgkppZy/y0DFxagQ=
github.com/microcosm-cc/bluemonday v1.0.1/go.mod h1:hsXNsILzKxV+sX77C5b8FSuKF00vh2OMYv+xgHpAMF4=
github.com/miekg/dns v1.1.68 h1:jsSRkNozw7G/mnmXULynzMNIsgY2dHC8LO6U6Ij2JEA=
github.com/miekg/dns v1.1.68/go.mod h1:fujopn7TB3Pu3JM69XaawiU0wqjpL9/8xGop5UrTPps=
github.com/mholt/acmez/v3 v3.1.4 h1:DyzZe/RnAzT3rpZj/2Ii5xZpiEvvYk3cQEN/RmqxwFQ=
github.com/mholt/acmez/v3 v3.1.4/go.mod h1:L1wOU06KKvq7tswuMDwKdcHeKpFFgkppZy/y0DFxagQ=
github.com/miekg/dns v1.1.69 h1:Kb7Y/1Jo+SG+a2GtfoFUfDkG//csdRPwRLkCsxDG9Sc=
github.com/miekg/dns v1.1.69/go.mod h1:7OyjD9nEba5OkqQ/hB4fy3PIoxafSZJtducccIelz3g=
github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
@@ -355,21 +300,16 @@ github.com/mitchellh/go-ps v1.0.0/go.mod h1:J4lOc8z8yJs6vUwklHw2XEIiT4z4C40KtWVN
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw=
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8=
github.com/mschoch/smat v0.2.0 h1:8imxQsjDm8yFEAVBe7azKmKSgzSkZXDuKkSq9374khM=
github.com/mschoch/smat v0.2.0/go.mod h1:kc9mz7DoBKqDyiRL7VZN8KvXQMWeTaVnttLRXOlotKw=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86/go.mod h1:kHJEU3ofeGjhHklVoIGuVj85JJwZ6kWPaJwCIxgnFmo=
github.com/neelance/sourcemap v0.0.0-20151028013722-8c68805598ab/go.mod h1:Qr6/a/Q4r9LP1IltGz7tA7iOK1WonHEYhu1HRBA7ZiM=
github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 h1:G7ERwszslrBzRxj//JalHPu/3yz+De2J+4aLtSRlHiY=
github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037/go.mod h1:2bpvgLBZEtENV5scfDFEtB/5+1M4hkQhDQrccEJ/qGw=
github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 h1:bQx3WeLcUWy+RletIKwUIt4x3t8n2SxavmoclizMb8c=
github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90/go.mod h1:y5+oSEHCPT/DGrS++Wc/479ERge0zTFxaF8PbGKcg2o=
github.com/openzipkin/zipkin-go v0.1.1/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTmOf0Erfk+hxe8=
github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 h1:onHthvaw9LFnH4t2DcNVpwGmV9E1BkGknEliJkfwQj0=
github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58/go.mod h1:DXv8WO4yhMYhSNPKjeNKa5WY9YCIEBRbNzFFPJbWO6Y=
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
@@ -389,75 +329,49 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRI
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g=
github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U=
github.com/prometheus/client_golang v0.8.0/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
github.com/prometheus/common v0.0.0-20180801064454-c7de2306084e/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs=
github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA=
github.com/prometheus/procfs v0.0.0-20180725123919-05ee40e3a273/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.17.0 h1:FuLQ+05u4ZI+SS/w9+BWEM2TXiHKsUQ9TADiRH7DuK0=
github.com/prometheus/procfs v0.17.0/go.mod h1:oPQLaDAMRbA+u8H5Pbfq+dl3VDAvHxMUOVhe0wYB2zw=
github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
github.com/quic-go/quic-go v0.54.0 h1:6s1YB9QotYI6Ospeiguknbp2Znb/jZYjZLRXn9kMQBg=
github.com/quic-go/quic-go v0.54.0/go.mod h1:e68ZEaCdyviluZmy44P6Iey98v/Wfz6HCjQEm+l8zTY=
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
github.com/prometheus/common v0.67.4 h1:yR3NqWO1/UyO1w2PhUvXlGQs/PtFmoveVO0KZ4+Lvsc=
github.com/prometheus/common v0.67.4/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI=
github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws=
github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw=
github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
github.com/quic-go/quic-go v0.57.1 h1:25KAAR9QR8KZrCZRThWMKVAwGoiHIrNbT72ULHTuI10=
github.com/quic-go/quic-go v0.57.1/go.mod h1:ly4QBAjHA2VhdnxhojRsCUOeJwKYg+taDlos92xb1+s=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA=
github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU=
github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU=
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sagikazarmark/locafero v0.10.0 h1:FM8Cv6j2KqIhM2ZK7HZjm4mpj9NBktLgowT1aN9q5Cc=
github.com/sagikazarmark/locafero v0.10.0/go.mod h1:Ieo3EUsjifvQu4NZwV5sPd4dwvu0OCgEQV7vjc9yDjw=
github.com/sagikazarmark/locafero v0.12.0 h1:/NQhBAkUb4+fH1jivKHWusDYFjMOOKU88eegjfxfHb4=
github.com/sagikazarmark/locafero v0.12.0/go.mod h1:sZh36u/YSZ918v0Io+U9ogLYQJ9tLLBmM4eneO6WwsI=
github.com/schollz/jsonstore v1.1.0 h1:WZBDjgezFS34CHI+myb4s8GGpir3UMpy7vWoCeO0n6E=
github.com/schollz/jsonstore v1.1.0/go.mod h1:15c6+9guw8vDRyozGjN3FoILt0wpruJk9Pi66vjaZfg=
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
github.com/shurcooL/component v0.0.0-20170202220835-f88ec8f54cc4/go.mod h1:XhFIlyj5a1fBNx5aJTbKoIq0mNaPvOagO+HjB3EtxrY=
github.com/shurcooL/events v0.0.0-20181021180414-410e4ca65f48/go.mod h1:5u70Mqkb5O5cxEA8nxTsgrgLehJeAw6Oc4Ab1c/P1HM=
github.com/shurcooL/github_flavored_markdown v0.0.0-20181002035957-2122de532470/go.mod h1:2dOwnU2uBioM+SGy2aZoq1f/Sd1l9OkAeAUvjSyvgU0=
github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk=
github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041/go.mod h1:N5mDOmsrJOB+vfqUK+7DmDyjhSLIIBnXo9lvZJj3MWQ=
github.com/shurcooL/gofontwoff v0.0.0-20180329035133-29b52fc0a18d/go.mod h1:05UtEgK5zq39gLST6uB0cf3NEHjETfB4Fgr3Gx5R9Vw=
github.com/shurcooL/gopherjslib v0.0.0-20160914041154-feb6d3990c2c/go.mod h1:8d3azKNyqcHP1GaQE/c6dDgjkgSx2BZ4IoEi4F1reUI=
github.com/shurcooL/highlight_diff v0.0.0-20170515013008-09bb4053de1b/go.mod h1:ZpfEhSmds4ytuByIcDnOLkTHGUI6KNqRNPDLHDk+mUU=
github.com/shurcooL/highlight_go v0.0.0-20181028180052-98c3abbbae20/go.mod h1:UDKB5a1T23gOMUJrI+uSuH0VRDStOiUVSjBTRDVBVag=
github.com/shurcooL/home v0.0.0-20181020052607-80b7ffcb30f9/go.mod h1:+rgNQw2P9ARFAs37qieuu7ohDNQ3gds9msbT2yn85sg=
github.com/shurcooL/htmlg v0.0.0-20170918183704-d01228ac9e50/go.mod h1:zPn1wHpTIePGnXSHpsVPWEktKXHr6+SS6x/IKRb7cpw=
github.com/shurcooL/httperror v0.0.0-20170206035902-86b7830d14cc/go.mod h1:aYMfkZ6DWSJPJ6c4Wwz3QtW22G7mf/PEgaB9k/ik5+Y=
github.com/shurcooL/httpfs v0.0.0-20171119174359-809beceb2371/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg=
github.com/shurcooL/httpgzip v0.0.0-20180522190206-b1c53ac65af9/go.mod h1:919LwcH0M7/W4fcZ0/jy0qGght1GIhqyS/EgWGH2j5Q=
github.com/shurcooL/issues v0.0.0-20181008053335-6292fdc1e191/go.mod h1:e2qWDig5bLteJ4fwvDAc2NHzqFEthkqn7aOZAOpj+PQ=
github.com/shurcooL/issuesapp v0.0.0-20180602232740-048589ce2241/go.mod h1:NPpHK2TI7iSaM0buivtFUc9offApnI0Alt/K8hcHy0I=
github.com/shurcooL/notifications v0.0.0-20181007000457-627ab5aea122/go.mod h1:b5uSkrEVM1jQUspwbixRBhaIjIzL2xazXp6kntxYle0=
github.com/shurcooL/octicon v0.0.0-20181028054416-fa4f57f9efb2/go.mod h1:eWdoE5JD4R5UVWDucdOPg1g2fqQRq78IQa9zlOV1vpQ=
github.com/shurcooL/reactions v0.0.0-20181006231557-f2e0b4ca5b82/go.mod h1:TCR1lToEk4d2s07G3XGfz2QrgHXg4RJBvjrOozvoWfk=
github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/shurcooL/users v0.0.0-20180125191416-49c67e49c537/go.mod h1:QJTqeLYEDaXHZDBsXlPCDqdhQuJkuw4NOtaxYe3xii4=
github.com/shurcooL/webdavfs v0.0.0-20170829043945-18c3829fa133/go.mod h1:hKmq5kWdCj2z2KEozexVbfEZIWiTjhE0+UjmZgPqehw=
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/slackhq/nebula v1.9.6 h1:Fl0LE2dHDeVEK3R+un59Z3V4ZzbZ6q2e/zF4ClaD5yo=
github.com/slackhq/nebula v1.9.6/go.mod h1:1+4q4wd3dDAjO8rKCttSb9JIVbklQhuJiBp5I0lbIsQ=
github.com/slackhq/nebula v1.9.7 h1:v5u46efIyYHGdfjFnozQbRRhMdaB9Ma1SSTcUcE2lfE=
github.com/slackhq/nebula v1.9.7/go.mod h1:1+4q4wd3dDAjO8rKCttSb9JIVbklQhuJiBp5I0lbIsQ=
github.com/smallstep/assert v0.0.0-20200723003110-82e2b9b3b262 h1:unQFBIznI+VYD1/1fApl1A+9VcBk+9dcqGfnePY87LY=
github.com/smallstep/assert v0.0.0-20200723003110-82e2b9b3b262/go.mod h1:MyOHs9Po2fbM1LHej6sBUT8ozbxmMOFG+E+rx/GSGuc=
github.com/smallstep/certificates v0.28.4 h1:JTU6/A5Xes6m+OsR6fw1RACSA362vJc9SOFVG7poBEw=
github.com/smallstep/certificates v0.28.4/go.mod h1:LUqo+7mKZE7FZldlTb0zhU4A0bq4G4+akieFMcTaWvA=
github.com/smallstep/cli-utils v0.12.1 h1:D9QvfbFqiKq3snGZ2xDcXEFrdFJ1mQfPHZMq/leerpE=
github.com/smallstep/cli-utils v0.12.1/go.mod h1:skV2Neg8qjiKPu2fphM89H9bIxNpKiiRTnX9Q6Lc+20=
github.com/smallstep/certificates v0.29.0 h1:f90szTKYTW62bmCc+qE5doGqIGPVxTQb8Ba37e/K8Zs=
github.com/smallstep/certificates v0.29.0/go.mod h1:27WI0od6gu84mvE4mYQ/QZGyYwHXvhsiSRNC+y3t+mo=
github.com/smallstep/cli-utils v0.12.2 h1:lGzM9PJrH/qawbzMC/s2SvgLdJPKDWKwKzx9doCVO+k=
github.com/smallstep/cli-utils v0.12.2/go.mod h1:uCPqefO29goHLGqFnwk0i8W7XJu18X3WHQFRtOm/00Y=
github.com/smallstep/go-attestation v0.4.4-0.20241119153605-2306d5b464ca h1:VX8L0r8vybH0bPeaIxh4NQzafKQiqvlOn8pmOXbFLO4=
github.com/smallstep/go-attestation v0.4.4-0.20241119153605-2306d5b464ca/go.mod h1:vNAduivU014fubg6ewygkAvQC0IQVXqdc8vaGl/0er4=
github.com/smallstep/linkedca v0.23.0 h1:5W/7EudlK1HcCIdZM68dJlZ7orqCCCyv6bm2l/0JmLU=
github.com/smallstep/linkedca v0.23.0/go.mod h1:7cyRM9soAYySg9ag65QwytcgGOM+4gOlkJ/YA58A9E8=
github.com/smallstep/linkedca v0.25.0 h1:txT9QHGbCsJq0MhAghBq7qhurGY727tQuqUi+n4BVBo=
github.com/smallstep/linkedca v0.25.0/go.mod h1:Q3jVAauFKNlF86W5/RFtgQeyDKz98GL/KN3KG4mJOvc=
github.com/smallstep/nosql v0.7.0 h1:YiWC9ZAHcrLCrayfaF+QJUv16I2bZ7KdLC3RpJcnAnE=
github.com/smallstep/nosql v0.7.0/go.mod h1:H5VnKMCbeq9QA6SRY5iqPylfxLfYcLwvUff3onQ8+HU=
github.com/smallstep/pkcs7 v0.2.1 h1:6Kfzr/QizdIuB6LSv8y1LJdZ3aPSfTNhTLqAx9CTLfA=
@@ -466,30 +380,26 @@ github.com/smallstep/scep v0.0.0-20250318231241-a25cabb69492 h1:k23+s51sgYix4Zgb
github.com/smallstep/scep v0.0.0-20250318231241-a25cabb69492/go.mod h1:QQhwLqCS13nhv8L5ov7NgusowENUtXdEzdytjmJHdZQ=
github.com/smallstep/truststore v0.13.0 h1:90if9htAOblavbMeWlqNLnO9bsjjgVv2hQeQJCi/py4=
github.com/smallstep/truststore v0.13.0/go.mod h1:3tmMp2aLKZ/OA/jnFUB0cYPcho402UG2knuJoPh4j7A=
github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d/go.mod h1:UdhH50NIW0fCiwBSr0co2m7BnFLdv4fQTgdqdJTHFeE=
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw=
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U=
github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e/go.mod h1:HuIsMU8RRBOtsCgI77wP899iHVBQpCmg4ErYMZB+2IA=
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI=
github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
github.com/spf13/afero v1.14.0 h1:9tH6MapGnn/j0eb0yIXiLjERO8RB6xIVZRDCX7PtqWA=
github.com/spf13/afero v1.14.0/go.mod h1:acJQ8t0ohCGuMN3O+Pv0V0hgMxNYDlvdk+VTfyZmbYo=
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/spf13/cast v1.9.2 h1:SsGfm7M8QOFtEzumm7UZrZdLLquNdzFYfIbEXntcFbE=
github.com/spf13/cast v1.9.2/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=
github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU=
github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s=
github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0=
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
github.com/spf13/viper v1.20.1 h1:ZMi+z/lvLyPSCoNtFCpqjy0S4kPbirhpTMwl8BkW9X4=
github.com/spf13/viper v1.20.1/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4=
github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU=
github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY=
github.com/stoewer/go-strcase v1.3.1 h1:iS0MdW+kVTxgMoE1LAZyMiYJFKlOzLooE4MxjirtkAs=
github.com/stoewer/go-strcase v1.3.1/go.mod h1:fAH5hQ5pehh+j3nZfvwdk2RgEgQjAoM8wodgtPmh1xo=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
@@ -509,14 +419,16 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
github.com/tailscale/tscert v0.0.0-20240608151842-d3f834017e53 h1:uxMgm0C+EjytfAqyfBG55ZONKQ7mvd7x4YYCWsf8QHQ=
github.com/tailscale/tscert v0.0.0-20240608151842-d3f834017e53/go.mod h1:kNGUQ3VESx3VZwRwA9MSCUegIl6+saPL8Noq82ozCaU=
github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA=
github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55 h1:Gzfnfk2TWrk8Jj4P4c1a3CtQyMaTVCznlkLZI++hok4=
github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55/go.mod h1:4k4QO+dQ3R5FofL+SanAUZe+/QfeK0+OIuwDIRu2vSg=
github.com/tailscale/tscert v0.0.0-20251216020129-aea342f6d747 h1:RnBbFMmodYzhC6adOjTbtUQXyzV8dcvKYbolzs6Qch0=
github.com/tailscale/tscert v0.0.0-20251216020129-aea342f6d747/go.mod h1:ejPAJui3kVK4u5TgMtqtXlWf5HnKh9fLy5kvpaeuas0=
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/match v1.2.0 h1:0pt8FlkOwjN2fPt4bIl4BoNxb98gGHN2ObFEDkrfZnM=
github.com/tidwall/match v1.2.0/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
@@ -529,8 +441,6 @@ github.com/unrolled/secure v1.17.0 h1:Io7ifFgo99Bnh0J7+Q+qcMzWM6kaDPCA5FroFZEdbW
github.com/unrolled/secure v1.17.0/go.mod h1:BmF5hyM6tXczk3MpQkFf1hpKSRqCyhqcbiQtiAF7+40=
github.com/urfave/cli v1.22.17 h1:SYzXoiPfQjHBbkYxbew5prZHS1TOLT3ierW8SYLqtVQ=
github.com/urfave/cli v1.22.17/go.mod h1:b0ht0aqgH/6pBYzzxURyrM4xXNgsoT/n2ZzwQiEhNVo=
github.com/viant/assertly v0.4.8/go.mod h1:aGifi++jvCrUaklKEKT0BU95igDNaqkvz+49uaYMPRU=
github.com/viant/toolbox v0.24.0/go.mod h1:OxMCG57V0PXuIP2HNQrtJf2CjqdmbrOx5EkMILuUhzM=
github.com/woodsbury/decimal128 v1.4.0 h1:xJATj7lLu4f2oObouMt2tgGiElE5gO6mSWUjQsBgUlc=
github.com/woodsbury/decimal128 v1.4.0/go.mod h1:BP46FUrVjVhdTbKT+XuQh2xfQaGki9LMIRJSFuh6THU=
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
@@ -552,41 +462,40 @@ github.com/zeebo/pcg v1.0.1 h1:lyqfGeWiv4ahac6ttHs+I5hwtH/+1mrhlCtVNQM2kHo=
github.com/zeebo/pcg v1.0.1/go.mod h1:09F0S9iiKrwn9rlI5yjLkmrug154/YRW6KnnXVDM/l4=
go.etcd.io/bbolt v1.4.3 h1:dEadXpI6G79deX5prL3QRNP6JB8UxVkqo4UPnHaNXJo=
go.etcd.io/bbolt v1.4.3/go.mod h1:tKQlpPaYCVFctUIgFKFnAlvbmB3tpy1vkTnDWohtc0E=
go.opencensus.io v0.18.0/go.mod h1:vKdFvxhtzZ9onBp9VKHK8z/sRpBMnKAsufL7wlDrCOA=
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 h1:q4XOmH/0opmeuJtPsbFNivyl7bCt7yRBbeEm2sC/XtQ=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0/go.mod h1:snMWehoOh2wsEwnvvwtDyFCxVeDAODenXHtn5vzrKjo=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0 h1:Hf9xI/XLML9ElpiHVDNwvqI0hIFlzV8dgIr35kV1kRU=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0/go.mod h1:NfchwuyNoMcZ5MLHwPrODwUF1HWCXWrL31s8gSAdIKY=
go.opentelemetry.io/contrib/propagators/autoprop v0.62.0 h1:1+EHlhAe/tukctfePZRrDruB9vn7MdwyC+rf36nUSPM=
go.opentelemetry.io/contrib/propagators/autoprop v0.62.0/go.mod h1:skzESZBY3IYcqJgImc+fwXQWflvVe+jZxoA/uw60NaI=
go.opentelemetry.io/contrib/propagators/aws v1.37.0 h1:cp8AFiM/qjBm10C/ATIRnEDXpD5MBknrA0ANw4T2/ss=
go.opentelemetry.io/contrib/propagators/aws v1.37.0/go.mod h1:Cy8Hk2E2iSGEbsLnPUdeigrexaAOAGIAmBFK919EQs0=
go.opentelemetry.io/contrib/propagators/b3 v1.37.0 h1:0aGKdIuVhy5l4GClAjl72ntkZJhijf2wg1S7b5oLoYA=
go.opentelemetry.io/contrib/propagators/b3 v1.37.0/go.mod h1:nhyrxEJEOQdwR15zXrCKI6+cJK60PXAkJ/jRyfhr2mg=
go.opentelemetry.io/contrib/propagators/jaeger v1.37.0 h1:pW+qDVo0jB0rLsNeaP85xLuz20cvsECUcN7TE+D8YTM=
go.opentelemetry.io/contrib/propagators/jaeger v1.37.0/go.mod h1:x7bd+t034hxLTve1hF9Yn9qQJlO/pP8H5pWIt7+gsFM=
go.opentelemetry.io/contrib/propagators/ot v1.37.0 h1:tVjnBF6EiTDMXoq2Xuc2vK0I7MTbEs05II/0j9mMK+E=
go.opentelemetry.io/contrib/propagators/ot v1.37.0/go.mod h1:MQjyNXtxAC8PGN9gzPtO4GY5zuP+RI3XX53uWbCTvEQ=
go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ=
go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0 h1:Ahq7pZmv87yiyn3jeFz/LekZmPLLdKejuO3NcK9MssM=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0/go.mod h1:MJTqhM0im3mRLw1i8uGHnCvUEeS7VwRyxlLC78PA18M=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.37.0 h1:EtFWSnwW9hGObjkIdmlnWSydO+Qs8OwzfzXLUPg4xOc=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.37.0/go.mod h1:QjUEoiGCPkvFZ/MjK6ZZfNOS6mfVEVKYE99dFhuN2LI=
go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE=
go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E=
go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI=
go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg=
go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc=
go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps=
go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4=
go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0=
go.opentelemetry.io/proto/otlp v1.7.1 h1:gTOMpGDb0WTBOP8JaO72iL3auEZhVmAQg4ipjOVAtj4=
go.opentelemetry.io/proto/otlp v1.7.1/go.mod h1:b2rVh6rfI/s2pHWNlB7ILJcRALpcNDzKhACevjI+ZnE=
go.step.sm/crypto v0.70.0 h1:Q9Ft7N637mucyZcHZd1+0VVQJVwDCKqcb9CYcYi7cds=
go.step.sm/crypto v0.70.0/go.mod h1:pzfUhS5/ue7ev64PLlEgXvhx1opwbhFCjkvlhsxVds0=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0 h1:ssfIgGNANqpVFCndZvcuyKbl0g+UAVcbBcqGkG28H0Y=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0/go.mod h1:GQ/474YrbE4Jx8gZ4q5I4hrhUzM6UPzyrqJYV2AqPoQ=
go.opentelemetry.io/contrib/propagators/autoprop v0.64.0 h1:VVrb1ErDD0Tlh/0K0rUqjky1e8AekjspTFN9sU2ekaA=
go.opentelemetry.io/contrib/propagators/autoprop v0.64.0/go.mod h1:QCsOQk+9Ep8Mkp4/aPtSzUT0dc8SaPYzBAE6o1jYuSE=
go.opentelemetry.io/contrib/propagators/aws v1.39.0 h1:IvNR8pAVGpkK1CHMjU/YE6B6TlnAPGFvogkMWRWU6wo=
go.opentelemetry.io/contrib/propagators/aws v1.39.0/go.mod h1:TUsFCERuGM4IGhJG9w+9l0nzmHUKHuaDYYNF6mtNgjY=
go.opentelemetry.io/contrib/propagators/b3 v1.39.0 h1:PI7pt9pkSnimWcp5sQhUA9OzLbc3Ba4sL+VEUTNsxrk=
go.opentelemetry.io/contrib/propagators/b3 v1.39.0/go.mod h1:5gV/EzPnfYIwjzj+6y8tbGW2PKWhcsz5e/7twptRVQY=
go.opentelemetry.io/contrib/propagators/jaeger v1.39.0 h1:Gz3yKzfMSEFzF0Vy5eIpu9ndpo4DhXMCxsLMF0OOApo=
go.opentelemetry.io/contrib/propagators/jaeger v1.39.0/go.mod h1:2D/cxxCqTlrday0rZrPujjg5aoAdqk1NaNyoXn8FJn8=
go.opentelemetry.io/contrib/propagators/ot v1.39.0 h1:vKTve1W/WKPVp1fzJamhCDDECt+5upJJ65bPyWoddGg=
go.opentelemetry.io/contrib/propagators/ot v1.39.0/go.mod h1:FH5VB2N19duNzh1Q8ks6CsZFyu3LFhNLiA9lPxyEkvU=
go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48=
go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0 h1:f0cb2XPmrqn4XMy9PNliTgRKJgS5WcL/u0/WRYGz4t0=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0/go.mod h1:vnakAaFckOMiMtOIhFI2MNH4FYrZzXCYxmb1LlhoGz8=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.39.0 h1:in9O8ESIOlwJAEGTkkf34DesGRAc/Pn8qJ7k3r/42LM=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.39.0/go.mod h1:Rp0EXBm5tfnv0WL+ARyO/PHBEaEAT8UUHQ6AGJcSq6c=
go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0=
go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs=
go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18=
go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE=
go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8=
go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew=
go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI=
go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA=
go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A=
go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4=
go.step.sm/crypto v0.75.0 h1:UAHYD6q6ggYyzLlIKHv1MCUVjZIesXRZpGTlRC/HSHw=
go.step.sm/crypto v0.75.0/go.mod h1:wwQ57+ajmDype9mrI/2hRyrvJd7yja5xVgWYqpUN3PE=
go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs=
go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
@@ -595,48 +504,34 @@ go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc=
go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
go.uber.org/zap/exp v0.3.0 h1:6JYzdifzYkGmTdRR59oYH+Ng7k49H9qVpWwNSsGJj3U=
go.uber.org/zap/exp v0.3.0/go.mod h1:5I384qq7XGxYyByIhHm6jg5CHkGY0nsTfbDLgDDlgJQ=
go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI=
go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU=
go4.org v0.0.0-20180809161055-417644f6feb5/go.mod h1:MkTOUMDaeVYJUOUsaDXIhWPZYa1yOyC1qaOBpL57BhE=
golang.org/x/build v0.0.0-20190111050920-041ab4dc3f9d/go.mod h1:OWs+y06UdEOHN4y+MfF/py+xQ/tYqIWW03b70/CG9Rw=
golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI=
golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8=
golang.org/x/crypto/x509roots/fallback v0.0.0-20250826074233-8f580defa01d h1:earVicuMOnaYH+t9T1PeRhC0LHHPGXXTDD7NEEP/Pb4=
golang.org/x/crypto/x509roots/fallback v0.0.0-20250826074233-8f580defa01d/go.mod h1:MEIPiCnxvQEjA4astfaKItNwEVZA5Ki+3+nyGbJ5N18=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b h1:DXr+pvt3nC887026GRP39Ej11UATqWDmWuS99x26cD0=
golang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b/go.mod h1:4QTo5u+SEIbbKW1RacMZq1YEfOBqeXa19JeshGi+zc4=
golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
golang.org/x/crypto/x509roots/fallback v0.0.0-20251210140736-7dacc380ba00 h1:qObov2/X4yIpr98j5t6samg3mMF12Rl4taUJd1rWj+c=
golang.org/x/crypto/x509roots/fallback v0.0.0-20251210140736-7dacc380ba00/go.mod h1:MEIPiCnxvQEjA4astfaKItNwEVZA5Ki+3+nyGbJ5N18=
golang.org/x/exp v0.0.0-20251209150349-8475f28825e9 h1:MDfG8Cvcqlt9XXrmEiD4epKn7VJHZO84hejP9Jmp0MM=
golang.org/x/exp v0.0.0-20251209150349-8475f28825e9/go.mod h1:EPRbTFwzwjXj9NpYyyrvenVh9Y+GFeEvMNh7Xuz7xgU=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.28.0 h1:gQBtGhjxykdjY9YhZpSlZIsbnaE2+PgjfLWUQTnoZ1U=
golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181029044818-c44066c5c816/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181106065722-10aee1819953/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190313220215-9f648a60d977/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI=
golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
@@ -645,19 +540,10 @@ golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/net v0.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I=
golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20181017192945-9dcd33a902f4/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
golang.org/x/perf v0.0.0-20180704124530-6e6d33e29852/go.mod h1:JLpeXjPJfIyPr5TlbXLkXWLhP8nz10XfvxElABhCtcw=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw=
golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -665,17 +551,12 @@ golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181029174526-d69651ed3497/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190316082340-a2f829d7f35f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -689,8 +570,8 @@ golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
@@ -700,10 +581,9 @@ golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s=
golang.org/x/term v0.35.0 h1:bZBVKBudEyhRcajGcNc3jIfWPqV4y/Kt2XcoigOWtDQ=
golang.org/x/term v0.35.0/go.mod h1:TPGtkTLesOwf2DE8CgVYiZinHAOuy5AYUYT1lENIZnA=
golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q=
golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
@@ -712,76 +592,46 @@ golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20181030000716-a0a13e073c7b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/tools v0.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE=
golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w=
golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA=
golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
google.golang.org/api v0.0.0-20180910000450-7ca32eb868bf/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0=
google.golang.org/api v0.0.0-20181030000543-1d582fd0359e/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0=
google.golang.org/api v0.1.0/go.mod h1:UGEZY7KEX120AnNLIHFMKIo4obdJhkp2tPbaPlQx13Y=
google.golang.org/api v0.248.0 h1:hUotakSkcwGdYUqzCRc5yGYsg4wXxpkKlW5ryVqvC1Y=
google.golang.org/api v0.248.0/go.mod h1:yAFUAF56Li7IuIQbTFoLwXTCI6XCFKueOlS7S9e4F9k=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20181029155118-b69ba1387ce2/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20181202183823-bd91e49a0898/go.mod h1:7Ep/1NZk928CDR8SjdVbjWNpdIf6nzjE3BTgJDr2Atg=
google.golang.org/genproto v0.0.0-20190306203927-b5d61aea6440/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/api v0.257.0 h1:8Y0lzvHlZps53PEaw+G29SsQIkuKrumGWs9puiexNAA=
google.golang.org/api v0.257.0/go.mod h1:4eJrr+vbVaZSqs7vovFd1Jb/A6ml6iw2e6FBYf3GAO4=
google.golang.org/genproto v0.0.0-20250603155806-513f23925822 h1:rHWScKit0gvAPuOnu87KpaYtjK5zBMLcULh7gxkCXu4=
google.golang.org/genproto v0.0.0-20250603155806-513f23925822/go.mod h1:HubltRL7rMh0LfnQPkMH4NPDFEWp0jw3vixw7jEM53s=
google.golang.org/genproto/googleapis/api v0.0.0-20250826171959-ef028d996bc1 h1:APHvLLYBhtZvsbnpkfknDZ7NyH4z5+ub/I0u8L3Oz6g=
google.golang.org/genproto/googleapis/api v0.0.0-20250826171959-ef028d996bc1/go.mod h1:xUjFWUnWDpZ/C0Gu0qloASKFb6f8/QXiiXhSPFsD668=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250826171959-ef028d996bc1 h1:pmJpJEvT846VzausCQ5d7KreSROcDqmO388w5YbnltA=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250826171959-ef028d996bc1/go.mod h1:GmFNa4BdJZ2a8G+wCe9Bg3wwThLrJun751XstdJt5Og=
google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio=
google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.75.0 h1:+TW+dqTd2Biwe6KKfhE5JpiYIBWq865PhKGSXiivqt4=
google.golang.org/grpc v1.75.0/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ=
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1 h1:F29+wU6Ee6qgu9TddPgooOdaqsxTMunOoj8KA5yuS5A=
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1/go.mod h1:5KF+wpkbTSbGcR9zteSqZV6fqFOWBl4Yde8En8MryZA=
google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc=
google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
google.golang.org/genproto/googleapis/api v0.0.0-20251213004720-97cd9d5aeac2 h1:7LRqPCEdE4TP4/9psdaB7F2nhZFfBiGJomA5sojLWdU=
google.golang.org/genproto/googleapis/api v0.0.0-20251213004720-97cd9d5aeac2/go.mod h1:+rXWjjaukWZun3mLfjmVnQi18E1AsFbDN9QdJ5YXLto=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2 h1:2I6GHUeJ/4shcDpoUlLs/2WPnhg7yJwvXtqcMJt9liA=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM=
google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig=
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.6.0 h1:6Al3kEFFP9VJhRz3DID6quisgPnTeZVr4lep9kkxdPA=
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.6.0/go.mod h1:QLvsjh0OIR0TYBeiu2bkWGTJBUNQ64st52iWj/yA93I=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0/go.mod h1:WDnlLJ4WF5VGsH/HVa3CI79GS0ol3YnhVnKP89i0kNg=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
grpc.go4.org v0.0.0-20170609214715-11d0a25b4919/go.mod h1:77eQGdRu53HpSqPFJFmuJdjuHRquDANNeA4x7B8WQ9o=
honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
howett.net/plist v1.0.1 h1:37GdZ8tP09Q35o9ych3ehygcsL+HqKSwzctveSlarvM=
howett.net/plist v1.0.1/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g=
sourcegraph.com/sourcegraph/go-diff v0.5.0/go.mod h1:kuch7UrkMzY0X+p9CRK03kfuPQ2zzQcaEFbx8wA8rck=
sourcegraph.com/sqs/pbtypes v0.0.0-20180604144634-d3ebe8f20ae4/go.mod h1:ketZ/q3QxT9HOBeFhu6RdvsftgpsbFHBF5Cas6cDKZ0=

20
caddy/hotreload-skip.go Normal file
View File

@@ -0,0 +1,20 @@
//go:build nowatcher || nomercure
package caddy
import (
"errors"
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
)
type hotReloadContext struct {
}
func (_ *FrankenPHPModule) configureHotReload(_ *FrankenPHPApp) error {
return nil
}
func (_ *FrankenPHPModule) unmarshalHotReload(d *caddyfile.Dispenser) error {
return errors.New("hot reload support disabled")
}

104
caddy/hotreload.go Normal file
View File

@@ -0,0 +1,104 @@
//go:build !nowatcher && !nomercure
package caddy
import (
"bytes"
"encoding/gob"
"errors"
"fmt"
"hash/fnv"
"net/url"
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
"github.com/dunglas/frankenphp"
)
const defaultHotReloadPattern = "./**/*.{css,env,gif,htm,html,jpg,jpeg,js,mjs,php,png,svg,twig,webp,xml,yaml,yml}"
type hotReloadContext struct {
// HotReload specifies files to watch for file changes to trigger hot reloads updates. Supports the glob syntax.
HotReload *hotReloadConfig `json:"hot_reload,omitempty"`
}
type hotReloadConfig struct {
Topic string `json:"topic"`
Watch []string `json:"watch"`
}
func (f *FrankenPHPModule) configureHotReload(app *FrankenPHPApp) error {
if f.HotReload == nil {
return nil
}
if f.mercureHub == nil {
return errors.New("unable to enable hot reloading: no Mercure hub configured")
}
if len(f.HotReload.Watch) == 0 {
f.HotReload.Watch = []string{defaultHotReloadPattern}
}
if f.HotReload.Topic == "" {
uid, err := uniqueID(f)
if err != nil {
return err
}
f.HotReload.Topic = "https://frankenphp.dev/hot-reload/" + uid
}
app.opts = append(app.opts, frankenphp.WithHotReload(f.HotReload.Topic, f.mercureHub, f.HotReload.Watch))
f.preparedEnv["FRANKENPHP_HOT_RELOAD\x00"] = "/.well-known/mercure?topic=" + url.QueryEscape(f.HotReload.Topic)
return nil
}
func (f *FrankenPHPModule) unmarshalHotReload(d *caddyfile.Dispenser) error {
f.HotReload = &hotReloadConfig{
Watch: d.RemainingArgs(),
}
for d.NextBlock(1) {
switch v := d.Val(); v {
case "topic":
if !d.NextArg() {
return d.ArgErr()
}
if f.HotReload == nil {
f.HotReload = &hotReloadConfig{}
}
f.HotReload.Topic = d.Val()
case "watch":
patterns := d.RemainingArgs()
if len(patterns) == 0 {
return d.ArgErr()
}
f.HotReload.Watch = append(f.HotReload.Watch, patterns...)
default:
return wrongSubDirectiveError("hot_reload", "topic, watch", v)
}
}
return nil
}
func uniqueID(s any) (string, error) {
var b bytes.Buffer
if err := gob.NewEncoder(&b).Encode(s); err != nil {
return "", fmt.Errorf("unable to generate unique name: %w", err)
}
h := fnv.New64a()
if _, err := h.Write(b.Bytes()); err != nil {
return "", fmt.Errorf("unable to generate unique name: %w", err)
}
return fmt.Sprintf("%016x", h.Sum64()), nil
}

88
caddy/hotreload_test.go Normal file
View File

@@ -0,0 +1,88 @@
//go:build !nowatcher && !nomercure
package caddy_test
import (
"context"
"net/http"
"net/url"
"os"
"path/filepath"
"strings"
"sync"
"testing"
"github.com/caddyserver/caddy/v2/caddytest"
"github.com/stretchr/testify/require"
)
func TestHotReload(t *testing.T) {
const topic = "https://frankenphp.dev/hot-reload/test"
u := "/.well-known/mercure?topic=" + url.QueryEscape(topic)
tmpDir := t.TempDir()
indexFile := filepath.Join(tmpDir, "index.php")
tester := caddytest.NewTester(t)
tester.InitServer(`
{
debug
skip_install_trust
admin localhost:2999
}
http://localhost:`+testPort+` {
mercure {
transport local
subscriber_jwt TestKey
anonymous
}
php_server {
root `+tmpDir+`
hot_reload {
topic `+topic+`
watch `+tmpDir+`/*.php
}
}
`, "caddyfile")
var connected, received sync.WaitGroup
connected.Add(1)
received.Go(func() {
cx, cancel := context.WithCancel(t.Context())
req, _ := http.NewRequest(http.MethodGet, "http://localhost:"+testPort+u, nil)
req = req.WithContext(cx)
resp := tester.AssertResponseCode(req, http.StatusOK)
connected.Done()
var receivedBody strings.Builder
buf := make([]byte, 1024)
for {
_, err := resp.Body.Read(buf)
require.NoError(t, err)
receivedBody.Write(buf)
if strings.Contains(receivedBody.String(), "index.php") {
cancel()
break
}
}
require.NoError(t, resp.Body.Close())
})
connected.Wait()
require.NoError(t, os.WriteFile(indexFile, []byte("<?=$_SERVER['FRANKENPHP_HOT_RELOAD'];"), 0644))
received.Wait()
tester.AssertGetResponse("http://localhost:"+testPort+"/index.php", http.StatusOK, u)
}

13
caddy/mercure-skip.go Normal file
View File

@@ -0,0 +1,13 @@
//go:build nomercure
package caddy
type mercureContext struct {
}
func (f *FrankenPHPModule) configureHotReload(_ *FrankenPHPApp) error {
return nil
}
func (f *FrankenPHPModule) assignMercureHub(_ caddy.Context) {
}

34
caddy/mercure.go Normal file
View File

@@ -0,0 +1,34 @@
//go:build !nomercure
package caddy
import (
"github.com/caddyserver/caddy/v2"
"github.com/dunglas/frankenphp"
"github.com/dunglas/mercure"
mercureCaddy "github.com/dunglas/mercure/caddy"
)
func init() {
mercureCaddy.AllowNoPublish = true
}
type mercureContext struct {
mercureHub *mercure.Hub
}
func (f *FrankenPHPModule) assignMercureHub(ctx caddy.Context) {
if f.mercureHub = mercureCaddy.FindHub(ctx.Modules()); f.mercureHub == nil {
return
}
opt := frankenphp.WithMercureHub(f.mercureHub)
f.mercureHubRequestOption = &opt
for i, wc := range f.Workers {
wc.mercureHub = f.mercureHub
wc.options = append(wc.options, frankenphp.WithWorkerMercureHub(wc.mercureHub))
f.Workers[i] = wc
}
}

View File

@@ -2,6 +2,7 @@ package caddy
import (
"encoding/json"
"errors"
"fmt"
"log/slog"
"net/http"
@@ -21,6 +22,8 @@ import (
"github.com/dunglas/frankenphp/internal/fastabs"
)
var serverHeader = []string{"FrankenPHP Caddy"}
// FrankenPHPModule represents the "php_server" and "php" directives in the Caddyfile
// they are responsible for forwarding requests to FrankenPHP via "ServeHTTP"
//
@@ -30,6 +33,9 @@ import (
// }
// }
type FrankenPHPModule struct {
mercureContext
hotReloadContext
// Root sets the root folder to the site. Default: `root` directive, or the path of the public directory of the embed app it exists.
Root string `json:"root,omitempty"`
// SplitPath sets the substrings for splitting the URI into two parts. The first matching substring will be used to split the "path info" from the path. The first piece is suffixed with the matching substring and will be assumed as the actual resource (CGI script) name. The second piece will be set to PATH_INFO for the CGI script to use. Default: `.php`.
@@ -45,6 +51,7 @@ type FrankenPHPModule struct {
preparedEnv frankenphp.PreparedEnv
preparedEnvNeedsReplacement bool
logger *slog.Logger
mercureHubRequestOption *frankenphp.RequestOption
}
// CaddyModule returns the Caddy module information.
@@ -70,6 +77,9 @@ func (f *FrankenPHPModule) Provision(ctx caddy.Context) error {
return fmt.Errorf(`expected ctx.App("frankenphp") to return *FrankenPHPApp, got nil`)
}
f.assignMercureHub(ctx)
loggerOpt := frankenphp.WithRequestLogger(f.logger)
for i, wc := range f.Workers {
// make the file path absolute from the public directory
// this can only be done if the root is defined inside php_server
@@ -81,6 +91,8 @@ func (f *FrankenPHPModule) Provision(ctx caddy.Context) error {
if f.Env != nil {
wc.inheritEnv(f.Env)
}
wc.requestOptions = append(wc.requestOptions, loggerOpt)
f.Workers[i] = wc
}
@@ -94,14 +106,13 @@ func (f *FrankenPHPModule) Provision(ctx caddy.Context) error {
if frankenphp.EmbeddedAppPath == "" {
f.Root = "{http.vars.root}"
} else {
rrs := false
f.Root = filepath.Join(frankenphp.EmbeddedAppPath, defaultDocumentRoot)
var rrs bool
f.ResolveRootSymlink = &rrs
}
} else {
if frankenphp.EmbeddedAppPath != "" && filepath.IsLocal(f.Root) {
f.Root = filepath.Join(frankenphp.EmbeddedAppPath, f.Root)
}
} else if frankenphp.EmbeddedAppPath != "" && filepath.IsLocal(f.Root) {
f.Root = filepath.Join(frankenphp.EmbeddedAppPath, f.Root)
}
if len(f.SplitPath) == 0 {
@@ -142,6 +153,10 @@ func (f *FrankenPHPModule) Provision(ctx caddy.Context) error {
}
}
if err := f.configureHotReload(fapp); err != nil {
return err
}
return nil
}
@@ -152,11 +167,15 @@ func needReplacement(s string) bool {
// ServeHTTP implements caddyhttp.MiddlewareHandler.
func (f *FrankenPHPModule) ServeHTTP(w http.ResponseWriter, r *http.Request, _ caddyhttp.Handler) error {
origReq := r.Context().Value(caddyhttp.OriginalRequestCtxKey).(http.Request)
repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer)
ctx := r.Context()
origReq := ctx.Value(caddyhttp.OriginalRequestCtxKey).(http.Request)
repl := ctx.Value(caddy.ReplacerCtxKey).(*caddy.Replacer)
var (
documentRootOption frankenphp.RequestOption
documentRoot string
)
var documentRootOption frankenphp.RequestOption
var documentRoot string
if f.resolvedDocumentRoot == "" {
documentRoot = repl.ReplaceKnown(f.Root, "")
if documentRoot == "" && frankenphp.EmbeddedAppPath != "" {
@@ -184,16 +203,39 @@ func (f *FrankenPHPModule) ServeHTTP(w http.ResponseWriter, r *http.Request, _ c
}
}
fr, err := frankenphp.NewRequestWithContext(
r,
documentRootOption,
frankenphp.WithRequestSplitPath(f.SplitPath),
frankenphp.WithRequestPreparedEnv(env),
frankenphp.WithOriginalRequest(&origReq),
frankenphp.WithWorkerName(workerName),
var (
err error
fr *http.Request
)
if err = frankenphp.ServeHTTP(w, fr); err != nil {
if f.mercureHubRequestOption == nil {
fr, err = frankenphp.NewRequestWithContext(
r,
documentRootOption,
frankenphp.WithRequestSplitPath(f.SplitPath),
frankenphp.WithRequestPreparedEnv(env),
frankenphp.WithOriginalRequest(&origReq),
frankenphp.WithWorkerName(workerName),
)
} else {
fr, err = frankenphp.NewRequestWithContext(
r,
documentRootOption,
frankenphp.WithRequestSplitPath(f.SplitPath),
frankenphp.WithRequestPreparedEnv(env),
frankenphp.WithOriginalRequest(&origReq),
frankenphp.WithWorkerName(workerName),
*f.mercureHubRequestOption,
)
}
if err != nil {
return caddyhttp.Error(http.StatusInternalServerError, err)
}
// TODO: set caddyhttp.ServerHeader when https://github.com/caddyserver/caddy/pull/7338 will be released
w.Header()["Server"] = serverHeader
if err = frankenphp.ServeHTTP(w, fr); err != nil && !errors.As(err, &frankenphp.ErrRejected{}) {
return caddyhttp.Error(http.StatusInternalServerError, err)
}
@@ -202,7 +244,6 @@ func (f *FrankenPHPModule) ServeHTTP(w http.ResponseWriter, r *http.Request, _ c
// UnmarshalCaddyfile implements caddyfile.Unmarshaler.
func (f *FrankenPHPModule) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
// First pass: Parse all directives except "worker"
for d.Next() {
for d.NextBlock(0) {
switch d.Val() {
@@ -244,15 +285,20 @@ func (f *FrankenPHPModule) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
f.ResolveRootSymlink = &v
case "worker":
wc, err := parseWorkerConfig(d)
wc, err := unmarshalWorker(d)
if err != nil {
return err
}
f.Workers = append(f.Workers, wc)
case "hot_reload":
if err := f.unmarshalHotReload(d); err != nil {
return err
}
default:
allowedDirectives := "root, split, env, resolve_root_symlink, worker"
return wrongSubDirectiveError("php or php_server", allowedDirectives, d.Val())
return wrongSubDirectiveError("php or php_server", "hot_reload, name, root, split, env, resolve_root_symlink, worker", d.Val())
}
}
}
@@ -261,7 +307,7 @@ func (f *FrankenPHPModule) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
fileNames := make(map[string]struct{}, len(f.Workers))
for _, w := range f.Workers {
if _, ok := fileNames[w.FileName]; ok {
return fmt.Errorf(`workers in a single "php_server" block must not have duplicate filenames: %q`, w.FileName)
return fmt.Errorf(`workers in a single "php" or "php_server" block must not have duplicate filenames: %q`, w.FileName)
}
if len(w.MatchPath) == 0 {
@@ -565,7 +611,7 @@ func parsePhpServer(h httpcaddyfile.Helper) ([]httpcaddyfile.ConfigValue, error)
// workers can also match a path without being in the public directory
// in this case we need to prepend the worker routes to the existing routes
func prependWorkerRoutes(routes caddyhttp.RouteList, h httpcaddyfile.Helper, f FrankenPHPModule, fsrv caddy.Module, disableFsrv bool) caddyhttp.RouteList {
allWorkerMatches := caddyhttp.MatchPath{}
var allWorkerMatches caddyhttp.MatchPath
for _, w := range f.Workers {
for _, path := range w.MatchPath {
allWorkerMatches = append(allWorkerMatches, path)
@@ -580,7 +626,7 @@ func prependWorkerRoutes(routes caddyhttp.RouteList, h httpcaddyfile.Helper, f F
if !disableFsrv {
routes = append(routes, caddyhttp.Route{
MatcherSetsRaw: []caddy.ModuleMap{
caddy.ModuleMap{
{
"file": h.JSON(fileserver.MatchFile{
TryFiles: []string{"{http.request.uri.path}"},
Root: f.Root,

View File

@@ -1,7 +1,6 @@
package caddy
import (
"errors"
"net/http"
"path/filepath"
"strconv"
@@ -23,12 +22,16 @@ import (
// }
// }
type workerConfig struct {
mercureContext
// Name for the worker. Default: the filename for FrankenPHPApp workers, always prefixed with "m#" for FrankenPHPModule workers.
Name string `json:"name,omitempty"`
// FileName sets the path to the worker script.
FileName string `json:"file_name,omitempty"`
// Num sets the number of workers to start.
Num int `json:"num,omitempty"`
// MaxThreads sets the maximum number of threads for this worker.
MaxThreads int `json:"max_threads,omitempty"`
// Env sets an extra environment variable to the given value. Can be specified more than once for multiple environment variables.
Env map[string]string `json:"env,omitempty"`
// Directories to watch for file changes
@@ -37,9 +40,12 @@ type workerConfig struct {
MatchPath []string `json:"match_path,omitempty"`
// MaxConsecutiveFailures sets the maximum number of consecutive failures before panicking (defaults to 6, set to -1 to never panick)
MaxConsecutiveFailures int `json:"max_consecutive_failures,omitempty"`
options []frankenphp.WorkerOption
requestOptions []frankenphp.RequestOption
}
func parseWorkerConfig(d *caddyfile.Dispenser) (workerConfig, error) {
func unmarshalWorker(d *caddyfile.Dispenser) (workerConfig, error) {
wc := workerConfig{}
if d.NextArg() {
wc.FileName = d.Val()
@@ -63,8 +69,7 @@ func parseWorkerConfig(d *caddyfile.Dispenser) (workerConfig, error) {
}
for d.NextBlock(1) {
v := d.Val()
switch v {
switch v := d.Val(); v {
case "name":
if !d.NextArg() {
return wc, d.ArgErr()
@@ -82,10 +87,21 @@ func parseWorkerConfig(d *caddyfile.Dispenser) (workerConfig, error) {
v, err := strconv.ParseUint(d.Val(), 10, 32)
if err != nil {
return wc, err
return wc, d.WrapErr(err)
}
wc.Num = int(v)
case "max_threads":
if !d.NextArg() {
return wc, d.ArgErr()
}
v, err := strconv.ParseUint(d.Val(), 10, 32)
if err != nil {
return wc, d.WrapErr(err)
}
wc.MaxThreads = int(v)
case "env":
args := d.RemainingArgs()
if len(args) != 2 {
@@ -96,18 +112,22 @@ func parseWorkerConfig(d *caddyfile.Dispenser) (workerConfig, error) {
}
wc.Env[args[0]] = args[1]
case "watch":
if !d.NextArg() {
patterns := d.RemainingArgs()
if len(patterns) == 0 {
// the default if the watch directory is left empty:
wc.Watch = append(wc.Watch, defaultWatchPattern)
} else {
wc.Watch = append(wc.Watch, d.Val())
wc.Watch = append(wc.Watch, patterns...)
}
case "match":
// provision the path so it's identical to Caddy match rules
// see: https://github.com/caddyserver/caddy/blob/master/modules/caddyhttp/matchers.go
caddyMatchPath := (caddyhttp.MatchPath)(d.RemainingArgs())
caddyMatchPath.Provision(caddy.Context{})
wc.MatchPath = ([]string)(caddyMatchPath)
if err := caddyMatchPath.Provision(caddy.Context{}); err != nil {
return wc, d.WrapErr(err)
}
wc.MatchPath = caddyMatchPath
case "max_consecutive_failures":
if !d.NextArg() {
return wc, d.ArgErr()
@@ -115,21 +135,20 @@ func parseWorkerConfig(d *caddyfile.Dispenser) (workerConfig, error) {
v, err := strconv.Atoi(d.Val())
if err != nil {
return wc, err
return wc, d.WrapErr(err)
}
if v < -1 {
return wc, errors.New("max_consecutive_failures must be >= -1")
return wc, d.Errf("max_consecutive_failures must be >= -1")
}
wc.MaxConsecutiveFailures = int(v)
wc.MaxConsecutiveFailures = v
default:
allowedDirectives := "name, file, num, env, watch, match, max_consecutive_failures"
return wc, wrongSubDirectiveError("worker", allowedDirectives, v)
return wc, wrongSubDirectiveError("worker", "name, file, num, env, watch, match, max_consecutive_failures, max_threads", v)
}
}
if wc.FileName == "" {
return wc, errors.New(`the "file" argument must be specified`)
return wc, d.Err(`the "file" argument must be specified`)
}
if frankenphp.EmbeddedAppPath != "" && filepath.IsLocal(wc.FileName) {
@@ -139,7 +158,7 @@ func parseWorkerConfig(d *caddyfile.Dispenser) (workerConfig, error) {
return wc, nil
}
func (wc workerConfig) inheritEnv(env map[string]string) {
func (wc *workerConfig) inheritEnv(env map[string]string) {
if wc.Env == nil {
wc.Env = make(map[string]string, len(env))
}
@@ -151,7 +170,7 @@ func (wc workerConfig) inheritEnv(env map[string]string) {
}
}
func (wc workerConfig) matchesPath(r *http.Request, documentRoot string) bool {
func (wc *workerConfig) matchesPath(r *http.Request, documentRoot string) bool {
// try to match against a pattern if one is assigned
if len(wc.MatchPath) != 0 {

30
cgi.go
View File

@@ -12,8 +12,10 @@ package frankenphp
// #include "frankenphp.h"
import "C"
import (
"context"
"crypto/tls"
"net"
"net/http"
"path/filepath"
"strings"
"unsafe"
@@ -66,7 +68,7 @@ var knownServerKeys = []string{
//
// TODO: handle this case https://github.com/caddyserver/caddy/issues/3718
// Inspired by https://github.com/caddyserver/caddy/blob/master/modules/caddyhttp/reverseproxy/fastcgi/fastcgi.go
func addKnownVariablesToServer(thread *phpThread, fc *frankenPHPContext, trackVarsArray *C.zval) {
func addKnownVariablesToServer(fc *frankenPHPContext, trackVarsArray *C.zval) {
request := fc.request
keys := mainThread.knownServerKeys
// Separate remote IP and port; more lenient than net.SplitHostPort
@@ -82,10 +84,8 @@ func addKnownVariablesToServer(thread *phpThread, fc *frankenPHPContext, trackVa
ip = strings.Replace(ip, "[", "", 1)
ip = strings.Replace(ip, "]", "", 1)
var https string
var sslProtocol string
var sslCipher string
var rs string
var https, sslProtocol, sslCipher, rs string
if request.TLS == nil {
rs = "http"
https = ""
@@ -186,8 +186,8 @@ func packCgiVariable(key *C.zend_string, value string) C.ht_key_value_pair {
return C.ht_key_value_pair{key, toUnsafeChar(value), C.size_t(len(value))}
}
func addHeadersToServer(fc *frankenPHPContext, trackVarsArray *C.zval) {
for field, val := range fc.request.Header {
func addHeadersToServer(ctx context.Context, request *http.Request, trackVarsArray *C.zval) {
for field, val := range request.Header {
if k := mainThread.commonHeaders[field]; k != nil {
v := strings.Join(val, ", ")
C.frankenphp_register_single(k, toUnsafeChar(v), C.size_t(len(v)), trackVarsArray)
@@ -196,7 +196,7 @@ func addHeadersToServer(fc *frankenPHPContext, trackVarsArray *C.zval) {
// if the header name could not be cached, it needs to be registered safely
// this is more inefficient but allows additional sanitizing by PHP
k := phpheaders.GetUnCommonHeader(field)
k := phpheaders.GetUnCommonHeader(ctx, field)
v := strings.Join(val, ", ")
C.frankenphp_register_variable_safe(toUnsafeChar(k), toUnsafeChar(v), C.size_t(len(v)), trackVarsArray)
}
@@ -212,11 +212,11 @@ func addPreparedEnvToServer(fc *frankenPHPContext, trackVarsArray *C.zval) {
//export go_register_variables
func go_register_variables(threadIndex C.uintptr_t, trackVarsArray *C.zval) {
thread := phpThreads[threadIndex]
fc := thread.getRequestContext()
fc := thread.frankenPHPContext()
if fc.request != nil {
addKnownVariablesToServer(thread, fc, trackVarsArray)
addHeadersToServer(fc, trackVarsArray)
addKnownVariablesToServer(fc, trackVarsArray)
addHeadersToServer(thread.context(), fc.request, trackVarsArray)
}
// The Prepared Environment is registered last and can overwrite any previous values
@@ -277,13 +277,13 @@ func splitPos(path string, splitPath []string) int {
// See: https://github.com/php/php-src/blob/345e04b619c3bc11ea17ee02cdecad6ae8ce5891/main/SAPI.h#L72
//
//export go_update_request_info
func go_update_request_info(threadIndex C.uintptr_t, info *C.sapi_request_info) C.bool {
func go_update_request_info(threadIndex C.uintptr_t, info *C.sapi_request_info) {
thread := phpThreads[threadIndex]
fc := thread.getRequestContext()
fc := thread.frankenPHPContext()
request := fc.request
if request == nil {
return C.bool(fc.worker != nil)
return
}
authUser, authPassword, ok := request.BasicAuth()
@@ -311,8 +311,6 @@ func go_update_request_info(threadIndex C.uintptr_t, info *C.sapi_request_info)
info.request_uri = thread.pinCString(request.URL.RequestURI())
info.proto_num = C.int(request.ProtoMajor*1000 + request.ProtoMinor)
return C.bool(fc.worker != nil)
}
// SanitizedPathJoin performs filepath.Join(root, reqPath) that

View File

@@ -2,6 +2,8 @@ package frankenphp
import (
"context"
"errors"
"fmt"
"log/slog"
"net/http"
"os"
@@ -12,6 +14,8 @@ import (
// frankenPHPContext provides contextual information about the Request to handle.
type frankenPHPContext struct {
mercureContext
documentRoot string
splitPath []string
env PreparedEnv
@@ -36,6 +40,11 @@ type frankenPHPContext struct {
startedAt time.Time
}
type contextHolder struct {
ctx context.Context
frankenPHPContext *frankenPHPContext
}
// fromContext extracts the frankenPHPContext from a context.
func fromContext(ctx context.Context) (fctx *frankenPHPContext, ok bool) {
fctx, ok = ctx.Value(contextKey).(*frankenPHPContext)
@@ -61,7 +70,7 @@ func NewRequestWithContext(r *http.Request, opts ...RequestOption) (*http.Reques
}
if fc.logger == nil {
fc.logger = logger
fc.logger = globalLogger
}
if fc.documentRoot == "" {
@@ -91,7 +100,7 @@ func NewRequestWithContext(r *http.Request, opts ...RequestOption) (*http.Reques
// newDummyContext creates a fake context from a request path
func newDummyContext(requestPath string, opts ...RequestOption) (*frankenPHPContext, error) {
r, err := http.NewRequest(http.MethodGet, requestPath, nil)
r, err := http.NewRequestWithContext(globalCtx, http.MethodGet, requestPath, nil)
if err != nil {
return nil, err
}
@@ -117,23 +126,25 @@ func (fc *frankenPHPContext) closeContext() {
}
// validate checks if the request should be outright rejected
func (fc *frankenPHPContext) validate() bool {
func (fc *frankenPHPContext) validate() error {
if strings.Contains(fc.request.URL.Path, "\x00") {
fc.rejectBadRequest("Invalid request path")
fc.reject(ErrInvalidRequestPath)
return false
return ErrInvalidRequestPath
}
contentLengthStr := fc.request.Header.Get("Content-Length")
if contentLengthStr != "" {
if contentLength, err := strconv.Atoi(contentLengthStr); err != nil || contentLength < 0 {
fc.rejectBadRequest("invalid Content-Length header: " + contentLengthStr)
e := fmt.Errorf("%w: %q", ErrInvalidContentLengthHeader, contentLengthStr)
return false
fc.reject(e)
return e
}
}
return true
return nil
}
func (fc *frankenPHPContext) clientHasClosed() bool {
@@ -149,16 +160,22 @@ func (fc *frankenPHPContext) clientHasClosed() bool {
}
}
// reject sends a response with the given status code and message
func (fc *frankenPHPContext) reject(statusCode int, message string) {
// reject sends a response with the given status code and error
func (fc *frankenPHPContext) reject(err error) {
if fc.isDone {
return
}
re := &ErrRejected{}
if !errors.As(err, re) {
// Should never happen
panic("only instance of ErrRejected can be passed to reject")
}
rw := fc.responseWriter
if rw != nil {
rw.WriteHeader(statusCode)
_, _ = rw.Write([]byte(message))
rw.WriteHeader(re.status)
_, _ = rw.Write([]byte(err.Error()))
if f, ok := rw.(http.Flusher); ok {
f.Flush()
@@ -167,7 +184,3 @@ func (fc *frankenPHPContext) reject(statusCode int, message string) {
fc.closeContext()
}
func (fc *frankenPHPContext) rejectBadRequest(message string) {
fc.reject(http.StatusBadRequest, message)
}

View File

@@ -1,5 +1,9 @@
package frankenphp
import (
"github.com/dunglas/frankenphp/internal/state"
)
// EXPERIMENTAL: ThreadDebugState prints the state of a single PHP thread - debugging purposes only
type ThreadDebugState struct {
Index int
@@ -23,7 +27,7 @@ func DebugState() FrankenPHPDebugState {
ReservedThreadCount: 0,
}
for _, thread := range phpThreads {
if thread.state.is(stateReserved) {
if thread.state.Is(state.Reserved) {
fullState.ReservedThreadCount++
continue
}
@@ -38,9 +42,9 @@ func threadDebugState(thread *phpThread) ThreadDebugState {
return ThreadDebugState{
Index: thread.threadIndex,
Name: thread.name(),
State: thread.state.name(),
IsWaiting: thread.state.isInWaitingState(),
IsBusy: !thread.state.isInWaitingState(),
WaitingSinceMilliseconds: thread.state.waitTime(),
State: thread.state.Name(),
IsWaiting: thread.state.IsInWaitingState(),
IsBusy: !thread.state.IsInWaitingState(),
WaitingSinceMilliseconds: thread.state.WaitTime(),
}
}

View File

@@ -32,7 +32,7 @@ RUN apk add --no-cache \
zlib-dev \
bison \
nss-tools \
# file watcher
# file watcher \
libstdc++ \
linux-headers \
# Dev tools \
@@ -48,8 +48,8 @@ RUN apk add --no-cache \
echo 'set auto-load safe-path /' > /root/.gdbinit
WORKDIR /usr/local/src/php
RUN git clone --branch=PHP-8.4 https://github.com/php/php-src.git . && \
# --enable-embed is only necessary to generate libphp.so, we don't use this SAPI directly
RUN git clone --branch=PHP-8.5 https://github.com/php/php-src.git . && \
# --enable-embed is necessary to generate libphp.so, but we don't use this SAPI directly
./buildconf --force && \
EXTENSION_DIR=/usr/lib/frankenphp/modules ./configure \
--enable-embed \
@@ -71,7 +71,7 @@ RUN git clone --branch=PHP-8.4 https://github.com/php/php-src.git . && \
# Install e-dant/watcher (necessary for file watching)
WORKDIR /usr/local/src/watcher
RUN git clone https://github.com/e-dant/watcher . && \
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release && \
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release && \
cmake --build build/ && \
cmake --install build

View File

@@ -50,7 +50,7 @@ RUN apt-get update && \
apt-get clean
WORKDIR /usr/local/src/php
RUN git clone --branch=PHP-8.4 https://github.com/php/php-src.git . && \
RUN git clone --branch=PHP-8.5 https://github.com/php/php-src.git . && \
# --enable-embed is only necessary to generate libphp.so, we don't use this SAPI directly
./buildconf --force && \
EXTENSION_DIR=/usr/lib/frankenphp/modules ./configure \

View File

@@ -7,13 +7,17 @@ variable "VERSION" {
}
variable "PHP_VERSION" {
default = "8.2,8.3,8.4"
default = "8.2,8.3,8.4,8.5"
}
variable "GO_VERSION" {
default = "1.25"
}
variable "SPC_OPT_BUILD_ARGS" {
default = ""
}
variable "SHA" {}
variable "LATEST" {
@@ -24,8 +28,13 @@ variable "CACHE" {
default = ""
}
variable "CI" {
# CI flag coming from the environment or --set; empty by default
default = ""
}
variable DEFAULT_PHP_VERSION {
default = "8.4"
default = "8.5"
}
function "tag" {
@@ -91,8 +100,7 @@ target "default" {
platforms = os == "alpine" ? [
"linux/amd64",
"linux/386",
# FIXME: armv6 doesn't build in GitHub actions because we use a custom Go build
#"linux/arm/v6",
"linux/arm/v6",
"linux/arm/v7",
"linux/arm64",
] : [
@@ -141,6 +149,8 @@ target "static-builder-musl" {
}
args = {
FRANKENPHP_VERSION = VERSION
CI = CI
SPC_OPT_BUILD_ARGS = SPC_OPT_BUILD_ARGS
}
secret = ["id=github-token,env=GITHUB_TOKEN"]
}
@@ -165,6 +175,8 @@ target "static-builder-gnu" {
args = {
FRANKENPHP_VERSION = VERSION
GO_VERSION = GO_VERSION
CI = CI
SPC_OPT_BUILD_ARGS = SPC_OPT_BUILD_ARGS
}
secret = ["id=github-token,env=GITHUB_TOKEN"]
}

View File

@@ -16,29 +16,62 @@ FrankenPHP 也可以用作独立的 Go 库,将 PHP 嵌入到任何使用 `net/
## 开始
### 独立二进制
我们为 Linux 和 macOS 提供包含 [PHP 8.4](https://www.php.net/releases/8.4/zh.php) 以及大多数常用 PHP 扩展的 FrankenPHP 静态二进制文件。
在 Windows 上,请使用 [WSL](https://learn.microsoft.com/windows/wsl/) 运行 FrankenPHP。
你可以 [下载 FrankenPHP](https://github.com/dunglas/frankenphp/releases),或将以下命令复制到终端中,自动安装适用于你平台的版本:
### 安装脚本
你可以将以下命令复制到终端中,自动安装适用于你平台的版本:
```console
curl https://frankenphp.dev/install.sh | sh
mv frankenphp /usr/local/bin/
```
要提供当前目录的内容,请运行:
### 独立二进制
我们为 Linux 和 macOS 提供用于开发的 FrankenPHP 静态二进制文件,
包含 [PHP 8.4](https://www.php.net/releases/8.4/zh.php) 以及大多数常用 PHP 扩展。
[下载 FrankenPHP](https://github.com/dunglas/frankenphp/releases)
**安装扩展:** 常见扩展已内置,无法再安装更多扩展。
### rpm 软件包
我们的维护者为所有使用 `dnf` 的系统提供 rpm 包。安装方式:
```console
frankenphp php-server
sudo dnf install https://rpm.henderkes.com/static-php-1-0.noarch.rpm
sudo dnf module enable php-zts:static-8.4 # 可用 8.2-8.5
sudo dnf install frankenphp
```
你还可以使用以下命令运行命令行脚本:
**安装扩展:** `sudo dnf install php-zts-<extension>`
对于默认不可用的扩展,请使用 [PIE](https://github.com/php/pie)
```console
frankenphp php-cli /path/to/your/script.php
sudo dnf install pie-zts
sudo pie-zts install asgrim/example-pie-extension
```
### deb 软件包
我们的维护者为所有使用 `apt` 的系统提供 deb 包。安装方式:
```console
sudo curl -fsSL https://key.henderkes.com/static-php.gpg -o /usr/share/keyrings/static-php.gpg && \
echo "deb [signed-by=/usr/share/keyrings/static-php.gpg] https://deb.henderkes.com/ stable main" | sudo tee /etc/apt/sources.list.d/static-php.list && \
sudo apt update
sudo apt install frankenphp
```
**安装扩展:** `sudo apt install php-zts-<extension>`
对于默认不可用的扩展,请使用 [PIE](https://github.com/php/pie)
```console
sudo apt install pie-zts
sudo pie-zts install asgrim/example-pie-extension
```
### Docker
@@ -68,12 +101,28 @@ FrankenPHP 也作为 [Homebrew](https://brew.sh) 软件包提供,适用于 mac
brew install dunglas/frankenphp/frankenphp
```
**安装扩展:** 使用 [PIE](https://github.com/php/pie)。
### 用法
要提供当前目录的内容,请运行:
```console
frankenphp php-server
```
你还可以使用以下命令运行命令行脚本:
```console
frankenphp php-cli /path/to/your/script.php
```
对于 deb 和 rpm 软件包,还可以启动 systemd 服务:
```console
sudo systemctl start frankenphp
```
## 文档
- [Classic 模式](classic.md)

View File

@@ -2,7 +2,7 @@
[FrankenPHP Docker 镜像](https://hub.docker.com/r/dunglas/frankenphp) 基于 [官方 PHP 镜像](https://hub.docker.com/_/php/)。提供适用于流行架构的 Debian 和 Alpine Linux 变体。推荐使用 Debian 变体。
提供 PHP 8.2、8.3 和 8.4 的变体。
提供 PHP 8.2、8.3、8.4 和 8.5 的变体。
标签遵循此模式:`dunglas/frankenphp:<frankenphp-version>-php<php-version>-<os>`

View File

@@ -54,7 +54,8 @@ composer dump-env prod
1. 在准备好的应用的存储库中创建一个名为 `static-build.Dockerfile` 的文件。
```dockerfile
FROM --platform=linux/amd64 dunglas/frankenphp:static-builder
FROM --platform=linux/amd64 dunglas/frankenphp:static-builder-gnu
# 如果你打算在 glibc 系统上运行该二进制文件,请使用 static-builder-gnu
# 复制应用代码
WORKDIR /go/src/app/dist/app

View File

@@ -270,7 +270,7 @@ $user->updateInfo(null, 25, null); // Name 和 active 为 null
### 声明常量
生成器支持使用两个指令将 Go 常量导出到 PHP`//export_php:const` 用于全局常量,`//export_php:classconstant` 用于类常量。这允许你在 Go 和 PHP 代码之间共享配置值、状态代码和其他常量。
生成器支持使用两个指令将 Go 常量导出到 PHP`//export_php:const` 用于全局常量,`//export_php:classconst` 用于类常量。这允许你在 Go 和 PHP 代码之间共享配置值、状态代码和其他常量。
#### 全局常量
@@ -292,25 +292,25 @@ const STATUS_ERROR = iota
#### 类常量
使用 `//export_php:classconstant ClassName` 指令创建属于特定 PHP 类的常量:
使用 `//export_php:classconst ClassName` 指令创建属于特定 PHP 类的常量:
```go
//export_php:classconstant User
//export_php:classconst User
const STATUS_ACTIVE = 1
//export_php:classconstant User
//export_php:classconst User
const STATUS_INACTIVE = 0
//export_php:classconstant User
//export_php:classconst User
const ROLE_ADMIN = "admin"
//export_php:classconstant Order
//export_php:classconst Order
const STATE_PENDING = iota
//export_php:classconstant Order
//export_php:classconst Order
const STATE_PROCESSING = iota
//export_php:classconstant Order
//export_php:classconst Order
const STATE_COMPLETED = iota
```
@@ -346,10 +346,10 @@ const STR_REVERSE = iota
//export_php:const
const STR_NORMAL = iota
//export_php:classconstant StringProcessor
//export_php:classconst StringProcessor
const MODE_LOWERCASE = 1
//export_php:classconstant StringProcessor
//export_php:classconst StringProcessor
const MODE_UPPERCASE = 2
//export_php:function repeat_this(string $str, int $count, int $mode): string

View File

@@ -88,7 +88,8 @@ php artisan octane:frankenphp
1. 在您的应用程序的存储库中创建一个名为 `static-build.Dockerfile` 的文件:
```dockerfile
FROM --platform=linux/amd64 dunglas/frankenphp:static-builder
FROM --platform=linux/amd64 dunglas/frankenphp:static-builder-gnu
# 如果你打算在 musl-libc 系统上运行该二进制文件,请使用 static-builder-musl
# 复制你的应用
WORKDIR /go/src/app/dist/app

View File

@@ -79,10 +79,11 @@ sudo make install
Some FrankenPHP features depend on optional system dependencies that must be installed.
Alternatively, these features can be disabled by passing build tags to the Go compiler.
| Feature | Dependency | Build tag to disable it |
| ------------------------------ | --------------------------------------------------------------------- | ----------------------- |
| Brotli compression | [Brotli](https://github.com/google/brotli) | nobrotli |
| Restart workers on file change | [Watcher C](https://github.com/e-dant/watcher/tree/release/watcher-c) | nowatcher |
| Feature | Dependency | Build tag to disable it |
| ------------------------------ | ------------------------------------------------------------------------------------------------------------ | ----------------------- |
| Brotli compression | [Brotli](https://github.com/google/brotli) | nobrotli |
| Restart workers on file change | [Watcher C](https://github.com/e-dant/watcher/tree/release/watcher-c) | nowatcher |
| [Mercure](mercure.md) | [Mercure Go library](https://pkg.go.dev/github.com/dunglas/mercure) (automatically installed, AGPL licensed) | nomercure |
## Compile the Go App
@@ -102,8 +103,13 @@ xcaddy build \
--output frankenphp \
--with github.com/dunglas/frankenphp/caddy \
--with github.com/dunglas/mercure/caddy \
--with github.com/dunglas/vulcain/caddy
--with github.com/dunglas/vulcain/caddy \
--with github.com/dunglas/caddy-cbrotli
# Add extra Caddy modules and FrankenPHP extensions here
# optionally, if you would like to compile from your frankenphp sources:
# --with github.com/dunglas/frankenphp=$(pwd) \
# --with github.com/dunglas/frankenphp/caddy=$(pwd)/caddy
```
> [!TIP]

View File

@@ -29,7 +29,7 @@ Depending on your installation method, FrankenPHP and the PHP interpreter will l
FrankenPHP:
- `/etc/frankenphp/Caddyfile`: the main configuration file
- `/etc/frankenphp/caddy.d/*.caddy`: additional configuration files that are loaded automatically
- `/etc/frankenphp/Caddyfile.d/*.caddyfile`: additional configuration files that are loaded automatically
PHP:
@@ -53,13 +53,12 @@ RUN cp $PHP_INI_DIR/php.ini-development $PHP_INI_DIR/php.ini
FrankenPHP:
- `/etc/frankenphp/Caddyfile`: the main configuration file
- `/etc/frankenphp/caddy.d/*.caddy`: additional configuration files that are loaded automatically
- `/etc/frankenphp/Caddyfile.d/*.caddyfile`: additional configuration files that are loaded automatically
PHP:
- `php.ini`: `/etc/frankenphp/php.ini` (a `php.ini` file with production presets is provided by default)
- additional configuration files: `/etc/frankenphp/php.d/*.ini`
- PHP extensions: `/usr/lib/frankenphp/modules/`
- `php.ini`: `/etc/php-zts/php.ini` (a `php.ini` file with production presets is provided by default)
- additional configuration files: `/etc/php-zts/conf.d/*.ini`
## Static binary

View File

@@ -4,7 +4,7 @@
Debian and Alpine Linux variants are provided for popular architectures.
Debian variants are recommended.
Variants for PHP 8.2, 8.3 and 8.4 are provided.
Variants for PHP 8.2, 8.3, 8.4 and 8.5 are provided.
The tags follow this pattern: `dunglas/frankenphp:<frankenphp-version>-php<php-version>-<os>`

View File

@@ -54,7 +54,8 @@ The easiest way to create a Linux binary is to use the Docker-based builder we p
1. Create a file named `static-build.Dockerfile` in the repository of your app:
```dockerfile
FROM --platform=linux/amd64 dunglas/frankenphp:static-builder
FROM --platform=linux/amd64 dunglas/frankenphp:static-builder-gnu
# If you intend to run the binary on musl-libc systems, use static-builder-musl instead
# Copy your app
WORKDIR /go/src/app/dist/app

View File

@@ -88,19 +88,20 @@ While some variable types have the same memory representation between C/PHP and
This table summarizes what you need to know:
| PHP type | Go type | Direct conversion | C to Go helper | Go to C helper | Class Methods Support |
| ------------------ | ----------------------------- | ----------------- | --------------------------------- | ---------------------------------- | --------------------- |
| `int` | `int64` | ✅ | - | - | ✅ |
| `?int` | `*int64` | ✅ | - | - | ✅ |
| `float` | `float64` | ✅ | - | - | ✅ |
| `?float` | `*float64` | ✅ | - | - | ✅ |
| `bool` | `bool` | ✅ | - | - | ✅ |
| `?bool` | `*bool` | ✅ | - | - | ✅ |
| `string`/`?string` | `*C.zend_string` | ❌ | `frankenphp.GoString()` | `frankenphp.PHPString()` | ✅ |
| `array` | `frankenphp.AssociativeArray` | ❌ | `frankenphp.GoAssociativeArray()` | `frankenphp.PHPAssociativeArray()` | ✅ |
| `array` | `map[string]any` | ❌ | `frankenphp.GoMap()` | `frankenphp.PHPMap()` | ✅ |
| `array` | `[]any` | ❌ | `frankenphp.GoPackedArray()` | `frankenphp.PHPPackedArray()` | ✅ |
| `mixed` | `any` | ❌ | `GoValue()` | `PHPValue()` | ❌ |
| `object` | `struct` | ❌ | _Not yet implemented_ | _Not yet implemented_ | ❌ |
|--------------------|-------------------------------|-------------------|-----------------------------------|------------------------------------|-----------------------|
| `int` | `int64` | ✅ | - | - | ✅ |
| `?int` | `*int64` | ✅ | - | - | ✅ |
| `float` | `float64` | ✅ | - | - | ✅ |
| `?float` | `*float64` | ✅ | - | - | ✅ |
| `bool` | `bool` | ✅ | - | - | ✅ |
| `?bool` | `*bool` | ✅ | - | - | ✅ |
| `string`/`?string` | `*C.zend_string` | ❌ | `frankenphp.GoString()` | `frankenphp.PHPString()` | ✅ |
| `array` | `frankenphp.AssociativeArray` | ❌ | `frankenphp.GoAssociativeArray()` | `frankenphp.PHPAssociativeArray()` | ✅ |
| `array` | `map[string]any` | ❌ | `frankenphp.GoMap()` | `frankenphp.PHPMap()` | ✅ |
| `array` | `[]any` | ❌ | `frankenphp.GoPackedArray()` | `frankenphp.PHPPackedArray()` | ✅ |
| `mixed` | `any` | ❌ | `GoValue()` | `PHPValue()` | ❌ |
| `callable` | `*C.zval` | ❌ | - | frankenphp.CallPHPCallable() | ❌ |
| `object` | `struct` | ❌ | _Not yet implemented_ | _Not yet implemented_ | ❌ |
> [!NOTE]
>
@@ -132,7 +133,7 @@ import (
)
// export_php:function process_data_ordered(array $input): array
func process_data_ordered_map(arr *C.zval) unsafe.Pointer {
func process_data_ordered_map(arr *C.zend_array) unsafe.Pointer {
// Convert PHP associative array to Go while keeping the order
associativeArray, err := frankenphp.GoAssociativeArray[any](unsafe.Pointer(arr))
if err != nil {
@@ -157,7 +158,7 @@ func process_data_ordered_map(arr *C.zval) unsafe.Pointer {
}
// export_php:function process_data_unordered(array $input): array
func process_data_unordered_map(arr *C.zval) unsafe.Pointer {
func process_data_unordered_map(arr *C.zend_array) unsafe.Pointer {
// Convert PHP associative array to a Go map without keeping the order
// ignoring the order will be more performant
goMap, err := frankenphp.GoMap[any](unsafe.Pointer(arr))
@@ -178,9 +179,9 @@ func process_data_unordered_map(arr *C.zval) unsafe.Pointer {
}
// export_php:function process_data_packed(array $input): array
func process_data_packed(arr *C.zval) unsafe.Pointer {
func process_data_packed(arr *C.zend_array) unsafe.Pointer {
// Convert PHP packed array to Go
goSlice, err := frankenphp.GoPackedArray(unsafe.Pointer(arr), false)
goSlice, err := frankenphp.GoPackedArray(unsafe.Pointer(arr))
if err != nil {
// handle error
}
@@ -211,6 +212,43 @@ func process_data_packed(arr *C.zval) unsafe.Pointer {
- `frankenphp.GoAssociativeArray(arr unsafe.Pointer, ordered bool) frankenphp.AssociativeArray` - Convert a PHP array to an ordered Go `AssociativeArray` (map with order)
- `frankenphp.GoMap(arr unsafe.Pointer) map[string]any` - Convert a PHP array to an unordered Go map
- `frankenphp.GoPackedArray(arr unsafe.Pointer) []any` - Convert a PHP array to a Go slice
- `frankenphp.IsPacked(zval *C.zend_array) bool` - Check if a PHP array is packed (indexed only) or associative (key-value pairs)
### Working with Callables
FrankenPHP provides a way to work with PHP callables using the `frankenphp.CallPHPCallable` helper. This allows you to call PHP functions or methods from Go code.
To showcase this, let's create our own `array_map()` function that takes a callable and an array, applies the callable to each element of the array, and returns a new array with the results:
```go
// export_php:function my_array_map(array $data, callable $callback): array
func my_array_map(arr *C.zend_array, callback *C.zval) unsafe.Pointer {
goSlice, err := frankenphp.GoPackedArray[any](unsafe.Pointer(arr))
if err != nil {
panic(err)
}
result := make([]any, len(goSlice))
for index, value := range goSlice {
result[index] = frankenphp.CallPHPCallable(unsafe.Pointer(callback), []interface{}{value})
}
return frankenphp.PHPPackedArray(result)
}
```
Notice how we use `frankenphp.CallPHPCallable()` to call the PHP callable passed as a parameter. This function takes a pointer to the callable and an array of arguments, and it returns the result of the callable execution. You can use the callable syntax you're used to:
```php
<?php
$result = my_array_map([1, 2, 3], function($x) { return $x * 2; });
// $result will be [2, 4, 6]
$result = my_array_map(['hello', 'world'], 'strtoupper');
// $result will be ['HELLO', 'WORLD']
```
### Declaring a Native PHP Class
@@ -352,7 +390,7 @@ This design ensures that your Go code has complete control over how the object's
### Declaring Constants
The generator supports exporting Go constants to PHP using two directives: `//export_php:const` for global constants and `//export_php:classconstant` for class constants. This allows you to share configuration values, status codes, and other constants between Go and PHP code.
The generator supports exporting Go constants to PHP using two directives: `//export_php:const` for global constants and `//export_php:classconst` for class constants. This allows you to share configuration values, status codes, and other constants between Go and PHP code.
#### Global Constants
@@ -376,27 +414,27 @@ const STATUS_ERROR = iota
#### Class Constants
Use the `//export_php:classconstant ClassName` directive to create constants that belong to a specific PHP class:
Use the `//export_php:classconst ClassName` directive to create constants that belong to a specific PHP class:
```go
package example
//export_php:classconstant User
//export_php:classconst User
const STATUS_ACTIVE = 1
//export_php:classconstant User
//export_php:classconst User
const STATUS_INACTIVE = 0
//export_php:classconstant User
//export_php:classconst User
const ROLE_ADMIN = "admin"
//export_php:classconstant Order
//export_php:classconst Order
const STATE_PENDING = iota
//export_php:classconstant Order
//export_php:classconst Order
const STATE_PROCESSING = iota
//export_php:classconstant Order
//export_php:classconst Order
const STATE_COMPLETED = iota
```
@@ -437,10 +475,10 @@ const STR_REVERSE = iota
//export_php:const
const STR_NORMAL = iota
//export_php:classconstant StringProcessor
//export_php:classconst StringProcessor
const MODE_LOWERCASE = 1
//export_php:classconstant StringProcessor
//export_php:classconst StringProcessor
const MODE_UPPERCASE = 2
//export_php:function repeat_this(string $str, int $count, int $mode): string

View File

@@ -16,30 +16,63 @@ Découvrez plus de détails sur ce serveur dapplication dans le replay de cet
## Pour Commencer
### Binaire autonome
Sur Windows, utilisez [WSL](https://learn.microsoft.com/windows/wsl/) pour exécuter FrankenPHP.
Si vous préférez ne pas utiliser Docker, nous fournissons des binaires autonomes de FrankenPHP pour Linux et macOS
contenant [PHP 8.4](https://www.php.net/releases/8.4/fr.php) et la plupart des extensions PHP populaires.
### Script d'installation
Sous Windows, utilisez [WSL](https://learn.microsoft.com/windows/wsl/) pour exécuter FrankenPHP.
[Téléchargez FrankenPHP](https://github.com/php/frankenphp/releases) ou copiez cette ligne dans votre terminal pour installer automatiquement la version appropriée à votre plateforme :
Vous pouvez copier cette ligne dans votre terminal pour installer automatiquement
une version adaptée à votre plateforme :
```console
curl https://frankenphp.dev/install.sh | sh
mv frankenphp /usr/local/bin/
```
Pour servir le contenu du répertoire courant, exécutez :
### Binaire autonome
Nous fournissons des binaires statiques de FrankenPHP pour le développement, pour Linux et macOS,
contenant [PHP 8.4](https://www.php.net/releases/8.4/fr.php) et la plupart des extensions PHP populaires.
[Télécharger FrankenPHP](https://github.com/php/frankenphp/releases)
**Installation d'extensions :** Les extensions les plus courantes sont incluses. Il n'est pas possible d'en installer davantage.
### Paquets rpm
Nos mainteneurs proposent des paquets rpm pour tous les systèmes utilisant `dnf`. Pour installer, exécutez :
```console
frankenphp php-server
sudo dnf install https://rpm.henderkes.com/static-php-1-0.noarch.rpm
sudo dnf module enable php-zts:static-8.4 # 8.2-8.5 disponibles
sudo dnf install frankenphp
```
Vous pouvez également exécuter des scripts en ligne de commande avec :
**Installation d'extensions :** `sudo dnf install php-zts-<extension>`
Pour les extensions non disponibles par défaut, utilisez [PIE](https://github.com/php/pie) :
```console
frankenphp php-cli /path/to/your/script.php
sudo dnf install pie-zts
sudo pie-zts install asgrim/example-pie-extension
```
### Paquets deb
Nos mainteneurs proposent des paquets deb pour tous les systèmes utilisant `apt`. Pour installer, exécutez :
```console
sudo curl -fsSL https://key.henderkes.com/static-php.gpg -o /usr/share/keyrings/static-php.gpg && \
echo "deb [signed-by=/usr/share/keyrings/static-php.gpg] https://deb.henderkes.com/ stable main" | sudo tee /etc/apt/sources.list.d/static-php.list && \
sudo apt update
sudo apt install frankenphp
```
**Installation d'extensions :** `sudo apt install php-zts-<extension>`
Pour les extensions non disponibles par défaut, utilisez [PIE](https://github.com/php/pie) :
```console
sudo apt install pie-zts
sudo pie-zts install asgrim/example-pie-extension
```
### Docker
@@ -69,12 +102,28 @@ Pour l'installer :
brew install dunglas/frankenphp/frankenphp
```
**Installation d'extensions :** Utilisez [PIE](https://github.com/php/pie).
### Utilisation
Pour servir le contenu du répertoire courant, exécutez :
```console
frankenphp php-server
```
Vous pouvez également exécuter des scripts en ligne de commande avec :
```console
frankenphp php-cli /path/to/your/script.php
```
Pour les paquets deb et rpm, vous pouvez aussi démarrer le service systemd :
```console
sudo systemctl start frankenphp
```
## Documentation
- [Le mode classique](classic.md)

View File

@@ -2,7 +2,7 @@
Les images Docker de [FrankenPHP](https://hub.docker.com/r/dunglas/frankenphp) sont basées sur les [images PHP officielles](https://hub.docker.com/_/php/). Des variantes Debian et Alpine Linux sont fournies pour les architectures populaires. Les variantes Debian sont recommandées.
Des variantes pour PHP 8.2, 8.3 et 8.4 sont disponibles. [Parcourir les tags](https://hub.docker.com/r/dunglas/frankenphp/tags).
Des variantes pour PHP 8.2, 8.3, 8.4 et 8.5 sont disponibles. [Parcourir les tags](https://hub.docker.com/r/dunglas/frankenphp/tags).
Les tags suivent le pattern suivant: `dunglas/frankenphp:<frankenphp-version>-php<php-version>-<os>`

View File

@@ -56,13 +56,13 @@ La manière la plus simple de créer un binaire Linux est d'utiliser le builder
1. Créez un fichier nommé `static-build.Dockerfile` dans le répertoire de votre application préparée :
```dockerfile
FROM --platform=linux/amd64 dunglas/frankenphp:static-builder
FROM --platform=linux/amd64 dunglas/frankenphp:static-builder-gnu
# Si vous envisagez d'exécuter le binaire sur des systèmes musl-libc, utilisez plutôt static-builder-musl
# Copy your app
WORKDIR /go/src/app/dist/app
COPY . .
# Build the static binary, be sure to select only the PHP extensions you want
WORKDIR /go/src/app/
RUN EMBED=dist/app/ ./build-static.sh
```

View File

@@ -47,10 +47,15 @@ tar xf php-*
Tout est maintenant configuré pour écrire votre fonction native en Go. Créez un nouveau fichier nommé `stringext.go`. Notre première fonction prendra une chaîne comme argument, le nombre de fois à la répéter, un booléen pour indiquer s'il faut inverser la chaîne, et retournera la chaîne résultante. Cela devrait ressembler à ceci :
```go
package example
// #include <Zend/zend_types.h>
import "C"
import (
"C"
"github.com/dunglas/frankenphp"
"strings"
"unsafe"
"github.com/dunglas/frankenphp"
)
//export_php:function repeat_this(string $str, int $count, bool $reverse): string
@@ -81,18 +86,20 @@ Alors que le premier point parle de lui-même, le second peut être plus diffici
Bien que certains types de variables aient la même représentation mémoire entre C/PHP et Go, certains types nécessitent plus de logique pour être directement utilisés. C'est peut-être la partie la plus difficile quand il s'agit d'écrire des extensions car cela nécessite de comprendre les fonctionnements internes du moteur Zend et comment les variables sont stockées dans le moteur de PHP. Ce tableau résume ce que vous devez savoir :
| Type PHP | Type Go | Conversion directe | Assistant C vers Go | Assistant Go vers C | Support des Méthodes de Classe |
| ------------------ | ------------------- | ------------------ | ----------------------- | ----------------------- | ------------------------------ |
| `int` | `int64` | ✅ | - | - | ✅ |
| `?int` | `*int64` | ✅ | - | - | ✅ |
| `float` | `float64` | ✅ | - | - | ✅ |
| `?float` | `*float64` | ✅ | - | - | ✅ |
| `bool` | `bool` | ✅ | - | - | ✅ |
| `?bool` | `*bool` | ✅ | - | - | ✅ |
| `string`/`?string` | `*C.zend_string` | ❌ | frankenphp.GoString() | frankenphp.PHPString() | ✅ |
| `array` | `*frankenphp.Array` | ❌ | frankenphp.GoArray() | frankenphp.PHPArray() | ✅ |
| `mixed` | `any` | ❌ | `GoValue()` | `PHPValue()` | |
| `object` | `struct` | ❌ | _Pas encore implémenté_ | _Pas encore implémenté_ | |
| Type PHP | Type Go | Conversion directe | Assistant C vers Go | Assistant Go vers C | Support des Méthodes de Classe |
| ------------------ | ----------------------------- | ------------------ | --------------------------------- | ---------------------------------- | ------------------------------ |
| `int` | `int64` | ✅ | - | - | ✅ |
| `?int` | `*int64` | ✅ | - | - | ✅ |
| `float` | `float64` | ✅ | - | - | ✅ |
| `?float` | `*float64` | ✅ | - | - | ✅ |
| `bool` | `bool` | ✅ | - | - | ✅ |
| `?bool` | `*bool` | ✅ | - | - | ✅ |
| `string`/`?string` | `*C.zend_string` | ❌ | frankenphp.GoString() | frankenphp.PHPString() | ✅ |
| `array` | `frankenphp.AssociativeArray` | ❌ | `frankenphp.GoAssociativeArray()` | `frankenphp.PHPAssociativeArray()` | ✅ |
| `array` | `map[string]any` | ❌ | `frankenphp.GoMap()` | `frankenphp.PHPMap()` | |
| `array` | `[]any` | ❌ | `frankenphp.GoPackedArray()` | `frankenphp.PHPPackedArray()` | |
| `mixed` | `any` | ❌ | `GoValue()` | `PHPValue()` | ❌ |
| `object` | `struct` | ❌ | _Pas encore implémenté_ | _Pas encore implémenté_ | ❌ |
> [!NOTE]
> Ce tableau n'est pas encore exhaustif et sera complété au fur et à mesure que l'API de types FrankenPHP deviendra plus complète.
@@ -103,61 +110,150 @@ Si vous vous référez à l'extrait de code de la section précédente, vous pou
#### Travailler avec les Tableaux
FrankenPHP fournit un support natif pour les tableaux PHP à travers le type `frankenphp.Array`. Ce type représente à la fois les tableaux indexés PHP (listes) et les tableaux associatifs (hashmaps) avec des paires clé-valeur ordonnées.
FrankenPHP fournit un support natif pour les tableaux PHP à travers `frankenphp.AssociativeArray` ou une conversion directe vers une map ou un slice.
`AssociativeArray` représente une [hash map](https://fr.wikipedia.org/wiki/Table_de_hachage) composée d'un champ `Map: map[string]any` et d'un champ optionnel `Order: []string` (contrairement aux "tableaux associatifs" PHP, les maps Go ne sont pas ordonnées).
Si l'ordre ou l'association ne sont pas nécessaires, il est également possible de convertir directement vers un slice `[]any` ou une map non ordonnée `map[string]any`.
**Créer et manipuler des tableaux en Go :**
```go
//export_php:function process_data(array $input): array
func process_data(arr *C.zval) unsafe.Pointer {
// Convertir le tableau PHP vers Go
goArray := frankenphp.GoArray(unsafe.Pointer(arr))
package example
result := &frankenphp.Array{}
// #include <Zend/zend_types.h>
import "C"
import (
"unsafe"
result.SetInt(0, "first")
result.SetInt(1, "second")
result.Append("third") // Assigne automatiquement la prochaine clé entière
"github.com/dunglas/frankenphp"
)
result.SetString("name", "John")
result.SetString("age", int64(30))
for i := uint32(0); i < goArray.Len(); i++ {
key, value := goArray.At(i)
if key.Type == frankenphp.PHPStringKey {
result.SetString("processed_"+key.Str, value)
} else {
result.SetInt(key.Int+100, value)
}
// export_php:function process_data_ordered(array $input): array
func process_data_ordered_map(arr *C.zval) unsafe.Pointer {
// Convertir le tableau associatif PHP vers Go en conservant l'ordre
associativeArray, err := frankenphp.GoAssociativeArray[any](unsafe.Pointer(arr))
if err != nil {
// gérer l'erreur
}
// Reconvertir vers un tableau PHP
return frankenphp.PHPArray(result)
// parcourir les entrées dans l'ordre
for _, key := range associativeArray.Order {
value, _ = associativeArray.Map[key]
// faire quelque chose avec key et value
}
// retourner un tableau ordonné
// si 'Order' n'est pas vide, seules les paires clé-valeur dans 'Order' seront respectées
return frankenphp.PHPAssociativeArray[string](frankenphp.AssociativeArray[string]{
Map: map[string]string{
"key1": "value1",
"key2": "value2",
},
Order: []string{"key1", "key2"},
})
}
// export_php:function process_data_unordered(array $input): array
func process_data_unordered_map(arr *C.zval) unsafe.Pointer {
// Convertir le tableau associatif PHP vers une map Go sans conserver l'ordre
// ignorer l'ordre sera plus performant
goMap, err := frankenphp.GoMap[any](unsafe.Pointer(arr))
if err != nil {
// gérer l'erreur
}
// parcourir les entrées sans ordre spécifique
for key, value := range goMap {
// faire quelque chose avec key et value
}
// retourner un tableau non ordonné
return frankenphp.PHPMap(map[string]string {
"key1": "value1",
"key2": "value2",
})
}
// export_php:function process_data_packed(array $input): array
func process_data_packed(arr *C.zval) unsafe.Pointer {
// Convertir le tableau packed PHP vers Go
goSlice, err := frankenphp.GoPackedArray(unsafe.Pointer(arr))
if err != nil {
// gérer l'erreur
}
// parcourir le slice dans l'ordre
for index, value := range goSlice {
// faire quelque chose avec index et value
}
// retourner un tableau packed
return frankenphp.PHPPackedArray([]string{"value1", "value2", "value3"})
}
```
**Fonctionnalités clés de `frankenphp.Array` :**
**Fonctionnalités clés de la conversion de tableaux :**
- **Paires clé-valeur ordonnées** - Maintient l'ordre d'insertion comme les tableaux PHP
- **Types de clés mixtes** - Supporte les clés entières et chaînes dans le même tableau
- **Sécurité de type** - Le type `PHPKey` assure une gestion appropriée des clés
- **Détection automatique de liste** - Lors de la conversion vers PHP, détecte automatiquement si le tableau doit être une liste compacte ou un hashmap
- **Les objets ne sont pas supportés** - Actuellement, seuls les types scalaires et les tableaux sont supportés. Passer un objet en tant qu'élément du tableau résultera d'une valeur `null` dans le tableau PHP.
- **Paires clé-valeur ordonnées** - Option pour conserver l'ordre du tableau associatif
- **Optimisé pour plusieurs cas** - Option de ne pas conserver l'ordre pour de meilleures performances ou conversion directe vers un slice
- **Détection automatique de liste** - Lors de la conversion vers PHP, détecte automatiquement si le tableau doit être une liste packed ou un hashmap
- **Tableaux imbriqués** - Les tableaux peuvent être imbriqués et convertiront automatiquement tous les types supportés (`int64`, `float64`, `string`, `bool`, `nil`, `AssociativeArray`, `map[string]any`, `[]any`)
- **Les objets ne sont pas supportés** - Actuellement, seuls les types scalaires et les tableaux peuvent être utilisés comme valeurs. Fournir un objet résultera en une valeur `null` dans le tableau PHP.
**Méthodes disponibles :**
##### Méthodes disponibles : Packed et Associatif
- `SetInt(key int64, value any)` - Définir une valeur avec une clé entière
- `SetString(key string, value any)` - Définir une valeur avec une clé chaîne
- `Append(value any)` - Ajouter une valeur avec la prochaine clé entière disponible
- `Len() uint32` - Obtenir le nombre d'éléments
- `At(index uint32) (PHPKey, any)` - Obtenir la paire clé-valeur à l'index
- `frankenphp.PHPArray(arr *frankenphp.Array) unsafe.Pointer` - Convertir vers un tableau PHP
- `frankenphp.PHPAssociativeArray(arr frankenphp.AssociativeArray) unsafe.Pointer` - Convertir vers un tableau PHP ordonné avec des paires clé-valeur
- `frankenphp.PHPMap(arr map[string]any) unsafe.Pointer` - Convertir une map vers un tableau PHP non ordonné avec des paires clé-valeur
- `frankenphp.PHPPackedArray(slice []any) unsafe.Pointer` - Convertir un slice vers un tableau PHP packed avec uniquement des valeurs indexées
- `frankenphp.GoAssociativeArray(arr unsafe.Pointer, ordered bool) frankenphp.AssociativeArray` - Convertir un tableau PHP vers un `AssociativeArray` Go ordonné (map avec ordre)
- `frankenphp.GoMap(arr unsafe.Pointer) map[string]any` - Convertir un tableau PHP vers une map Go non ordonnée
- `frankenphp.GoPackedArray(arr unsafe.Pointer) []any` - Convertir un tableau PHP vers un slice Go
- `frankenphp.IsPacked(zval *C.zend_array) bool` - Vérifie si le tableau PHP est une liste ou un tableau associatif
### Travailler avec des Callables
FrankenPHP propose un moyen de travailler avec les _callables_ PHP grâce au helper `frankenphp.CallPHPCallable()`. Cela permet d'appeler des fonctions ou des méthodes PHP depuis du code Go.
Pour illustrer cela, créons notre propre fonction `array_map()` qui prend un _callable_ et un tableau, applique le _callable_ à chaque élément du tableau, et retourne un nouveau tableau avec les résultats :
```go
// export_php:function my_array_map(array $data, callable $callback): array
func my_array_map(arr *C.zend_array, callback *C.zval) unsafe.Pointer {
goSlice, err := frankenphp.GoPackedArray[any](unsafe.Pointer(arr))
if err != nil {
panic(err)
}
result := make([]any, len(goSlice))
for index, value := range goSlice {
result[index] = frankenphp.CallPHPCallable(unsafe.Pointer(callback), []interface{}{value})
}
return frankenphp.PHPPackedArray(result)
}
```
Remarquez comment nous utilisons `frankenphp.CallPHPCallable()` pour appeler le _callable_ PHP passé en paramètre. Cette fonction prend un pointeur vers le _callable_ et un tableau d'arguments, et elle retourne le résultat de l'exécution du _callable_. Vous pouvez utiliser la syntaxe habituelle des _callables_ :
```php
<?php
$result = my_array_map([1, 2, 3], function($x) { return $x * 2; });
// $result vaudra [2, 4, 6]
$result = my_array_map(['hello', 'world'], 'strtoupper');
// $result vaudra ['HELLO', 'WORLD']
```
### Déclarer une Classe PHP Native
Le générateur prend en charge la déclaration de **classes opaques** comme structures Go, qui peuvent être utilisées pour créer des objets PHP. Vous pouvez utiliser la directive `//export_php:class` pour définir une classe PHP. Par exemple :
```go
package example
//export_php:class User
type UserStruct struct {
Name string
@@ -182,6 +278,16 @@ Cette approche fournit une meilleure encapsulation et empêche le code PHP de co
Puisque les propriétés ne sont pas directement accessibles, vous **devez définir des méthodes** pour interagir avec vos classes opaques. Utilisez la directive `//export_php:method` pour définir cela :
```go
package example
// #include <Zend/zend_types.h>
import "C"
import (
"unsafe"
"github.com/dunglas/frankenphp"
)
//export_php:class User
type UserStruct struct {
Name string
@@ -214,19 +320,29 @@ func (us *UserStruct) SetNamePrefix(prefix *C.zend_string) {
Le générateur prend en charge les paramètres nullables en utilisant le préfixe `?` dans les signatures PHP. Quand un paramètre est nullable, il devient un pointeur dans votre fonction Go, vous permettant de vérifier si la valeur était `null` en PHP :
```go
package example
// #include <Zend/zend_types.h>
import "C"
import (
"unsafe"
"github.com/dunglas/frankenphp"
)
//export_php:method User::updateInfo(?string $name, ?int $age, ?bool $active): void
func (us *UserStruct) UpdateInfo(name *C.zend_string, age *int64, active *bool) {
// $name est null?
// Vérifier si name a été fourni (pas null)
if name != nil {
us.Name = frankenphp.GoString(unsafe.Pointer(name))
}
// $age est null?
// Vérifier si age a été fourni (pas null)
if age != nil {
us.Age = int(*age)
}
// $active est null?
// Vérifier si active a été fourni (pas null)
if active != nil {
us.Active = *active
}
@@ -270,13 +386,15 @@ Cette conception garantit que votre code Go a un contrôle complet sur la façon
### Déclarer des Constantes
Le générateur prend en charge l'exportation de constantes Go vers PHP en utilisant deux directives : `//export_php:const` pour les constantes globales et `//export_php:classconstant` pour les constantes de classe. Cela vous permet de partager des valeurs de configuration, des codes de statut et d'autres constantes entre le code Go et PHP.
Le générateur prend en charge l'exportation de constantes Go vers PHP en utilisant deux directives : `//export_php:const` pour les constantes globales et `//export_php:classconst` pour les constantes de classe. Cela vous permet de partager des valeurs de configuration, des codes de statut et d'autres constantes entre le code Go et PHP.
#### Constantes Globales
Utilisez la directive `//export_php:const` pour créer des constantes PHP globales :
```go
package example
//export_php:const
const MAX_CONNECTIONS = 100
@@ -292,25 +410,27 @@ const STATUS_ERROR = iota
#### Constantes de Classe
Utilisez la directive `//export_php:classconstant ClassName` pour créer des constantes qui appartiennent à une classe PHP spécifique :
Utilisez la directive `//export_php:classconst ClassName` pour créer des constantes qui appartiennent à une classe PHP spécifique :
```go
//export_php:classconstant User
package example
//export_php:classconst User
const STATUS_ACTIVE = 1
//export_php:classconstant User
//export_php:classconst User
const STATUS_INACTIVE = 0
//export_php:classconstant User
//export_php:classconst User
const ROLE_ADMIN = "admin"
//export_php:classconstant Order
//export_php:classconst Order
const STATE_PENDING = iota
//export_php:classconstant Order
//export_php:classconst Order
const STATE_PROCESSING = iota
//export_php:classconstant Order
//export_php:classconst Order
const STATE_COMPLETED = iota
```
@@ -334,10 +454,15 @@ La directive prend en charge divers types de valeurs incluant les chaînes, enti
Vous pouvez utiliser les constantes comme vous êtes habitué dans le code Go. Par exemple, prenons la fonction `repeat_this()` que nous avons déclarée plus tôt et changeons le dernier argument en entier :
```go
package example
// #include <Zend/zend_types.h>
import "C"
import (
"C"
"github.com/dunglas/frankenphp"
"strings"
"unsafe"
"github.com/dunglas/frankenphp"
)
//export_php:const
@@ -346,10 +471,10 @@ const STR_REVERSE = iota
//export_php:const
const STR_NORMAL = iota
//export_php:classconstant StringProcessor
//export_php:classconst StringProcessor
const MODE_LOWERCASE = 1
//export_php:classconstant StringProcessor
//export_php:classconst StringProcessor
const MODE_UPPERCASE = 2
//export_php:function repeat_this(string $str, int $count, int $mode): string
@@ -398,9 +523,13 @@ Utilisez la directive `//export_php:namespace` en haut de votre fichier Go pour
```go
//export_php:namespace My\Extension
package main
package example
import "C"
import (
"unsafe"
"github.com/dunglas/frankenphp"
)
//export_php:function hello(): string
func hello() string {
@@ -500,16 +629,17 @@ Nous allons voir comment écrire une extension PHP simple en Go qui définit une
#### Définir la Fonction Go
Dans votre module Go vide, vous devez définir une nouvelle fonction native qui sera appelée depuis PHP. Pour ce faire, créez un fichier avec le nom que vous voulez, par exemple, `extension.go`, et ajoutez le code suivant :
Dans votre module, vous devez définir une nouvelle fonction native qui sera appelée depuis PHP. Pour ce faire, créez un fichier avec le nom que vous voulez, par exemple, `extension.go`, et ajoutez le code suivant :
```go
package ext_go
package example
//#include "extension.h"
// #include "extension.h"
import "C"
import (
"log/slog"
"unsafe"
"github.com/caddyserver/caddy/v2"
"github.com/dunglas/frankenphp"
)
@@ -520,7 +650,7 @@ func init() {
//export go_print_something
func go_print_something() {
go func() {
caddy.Log().Info("Hello from a goroutine!")
slog.Info("Hello from a goroutine!")
}()
}
```
@@ -697,7 +827,16 @@ Il ne reste qu'une chose à faire : implémenter la fonction `go_upper` en Go.
Notre fonction Go prendra un `*C.zend_string` comme paramètre, le convertira en chaîne Go en utilisant la fonction d'aide de FrankenPHP, le traitera, et retournera le résultat comme un nouveau `*C.zend_string`. Les fonctions d'aide gèrent toute la complexité de gestion de mémoire et de conversion pour nous.
```go
import "strings"
package example
// #include <Zend/zend_types.h>
import "C"
import (
"unsafe"
"strings"
"github.com/dunglas/frankenphp"
)
//export go_upper
func go_upper(s *C.zend_string) *C.zend_string {

View File

@@ -88,7 +88,8 @@ Suivez ces étapes pour empaqueter votre application Laravel en tant que binaire
1. Créez un fichier nommé `static-build.Dockerfile` dans le dépôt de votre application :
```dockerfile
FROM --platform=linux/amd64 dunglas/frankenphp:static-builder
FROM --platform=linux/amd64 dunglas/frankenphp:static-builder-gnu
# Si vous avez l'intention d'exécuter le binaire sur des systèmes musl-libc, utilisez plutôt static-builder-musl
# Copiez votre application
WORKDIR /go/src/app/dist/app

View File

@@ -37,6 +37,9 @@ frankenphp php-server --worker /path/to/your/worker/script.php --watch="/path/to
## Runtime Symfony
> [!TIP]
> La section suivante est nécessaire uniquement avant Symfony 7.4, où le support natif du mode worker de FrankenPHP a été introduit.
Le mode worker de FrankenPHP est pris en charge par le [Composant Runtime de Symfony](https://symfony.com/doc/current/components/runtime.html).
Pour démarrer une application Symfony dans un worker, installez le package FrankenPHP de [PHP Runtime](https://github.com/php-runtime/runtime) :

View File

@@ -16,31 +16,62 @@ FrankenPHPはあらゆるPHPアプリと連携し、ワーカーモードの公
## はじめに
### スタンドアロンバイナリ
LinuxとmacOS向けに、[PHP 8.4](https://www.php.net/releases/8.4/en.php)と人気のPHP拡張モジュールを含む静的な
FrankenPHPバイナリを提供しています。
Windowsをお使いの場合は、[WSL](https://learn.microsoft.com/windows/wsl/)を使用してFrankenPHPを実行してください。
[FrankenPHPをダウンロード](https://github.com/php/frankenphp/releases)するか、以下のコマンドを
ターミナルにコピーして実行すると、環境に合ったバージョンが自動的にインストールされます:
### インストールスクリプト
以下のコマンドをターミナルに貼り付けると、環境に合ったバージョンが自動的にインストールされます:
```console
curl https://frankenphp.dev/install.sh | sh
mv frankenphp /usr/local/bin/
```
現在のディレクトリのコンテンツを配信するには、以下を実行してください:
### スタンドアロンバイナリ
LinuxとmacOS向けに、開発用途の静的FrankenPHPバイナリを提供しています。
[PHP 8.4](https://www.php.net/releases/8.4/en.php)と主要なPHP拡張が含まれます。
[FrankenPHPをダウンロード](https://github.com/php/frankenphp/releases)
**拡張のインストール:** よく使われる拡張は同梱されています。追加の拡張をインストールすることはできません。
### rpm パッケージ
メンテナーが `dnf` を使用するすべてのシステム向けに rpm パッケージを提供しています。インストール方法:
```console
frankenphp php-server
sudo dnf install https://rpm.henderkes.com/static-php-1-0.noarch.rpm
sudo dnf module enable php-zts:static-8.4 # 8.2-8.5 利用可能
sudo dnf install frankenphp
```
コマンドラインスクリプトも実行できます:
**拡張のインストール:** `sudo dnf install php-zts-<extension>`
デフォルトで提供されていない拡張については [PIE](https://github.com/php/pie) を使用してください:
```console
frankenphp php-cli /path/to/your/script.php
sudo dnf install pie-zts
sudo pie-zts install asgrim/example-pie-extension
```
### deb パッケージ
メンテナーが `apt` を使用するすべてのシステム向けに deb パッケージを提供しています。インストール方法:
```console
sudo curl -fsSL https://key.henderkes.com/static-php.gpg -o /usr/share/keyrings/static-php.gpg && \
echo "deb [signed-by=/usr/share/keyrings/static-php.gpg] https://deb.henderkes.com/ stable main" | sudo tee /etc/apt/sources.list.d/static-php.list && \
sudo apt update
sudo apt install frankenphp
```
**拡張のインストール:** `sudo apt install php-zts-<extension>`
デフォルトで提供されていない拡張については [PIE](https://github.com/php/pie) を使用してください:
```console
sudo apt install pie-zts
sudo pie-zts install asgrim/example-pie-extension
```
### Docker
@@ -70,12 +101,28 @@ FrankenPHPはmacOSおよびLinux向けに[Homebrew](https://brew.sh)パッケー
brew install dunglas/frankenphp/frankenphp
```
**拡張のインストール:** [PIE](https://github.com/php/pie) を使用してください。
### 使い方
現在のディレクトリのコンテンツを配信するには、以下を実行してください:
```console
frankenphp php-server
```
コマンドラインスクリプトも実行できます:
```console
frankenphp php-cli /path/to/your/script.php
```
deb / rpm パッケージの場合は、systemd サービスを起動することもできます:
```console
sudo systemctl start frankenphp
```
## ドキュメント
- [クラシックモード](https://frankenphp.dev/docs/classic/)

View File

@@ -2,7 +2,7 @@
[FrankenPHPのDockerイメージ](https://hub.docker.com/r/dunglas/frankenphp)は、[公式PHPイメージ](https://hub.docker.com/_/php/)をベースにしています。主要なアーキテクチャに対してDebianとAlpine Linuxのバリアントを提供しており、Debianバリアントの使用を推奨しています。
PHP 8.2、8.3、8.4向けのバリアントが提供されています。
PHP 8.2、8.3、8.4、8.5向けのバリアントが提供されています。
タグは次のパターンに従います:`dunglas/frankenphp:<frankenphp-version>-php<php-version>-<os>`

View File

@@ -54,7 +54,8 @@ Linux用バイナリを作成する最も簡単な方法は、提供されてい
1. アプリのリポジトリに`static-build.Dockerfile`というファイルを作成します:
```dockerfile
FROM --platform=linux/amd64 dunglas/frankenphp:static-builder
FROM --platform=linux/amd64 dunglas/frankenphp:static-builder-gnu
# バイナリをmusl-libcシステムで実行する場合は、static-builder-musl を使用してください
# アプリをコピー
WORKDIR /go/src/app/dist/app

View File

@@ -218,7 +218,7 @@ $user->updateInfo(null, 25, null); // Nameとactiveがnull
### 定数の宣言
ジェネレーターは、2つのディレクティブを使用してGo定数をPHPにエクスポートすることをサポートしていますグローバル定数用の`//export_php:const`とクラス定数用の`//export_php:classconstant`です。これにより、GoとPHPコード間で設定値、ステータスコード、その他の定数を共有できます。
ジェネレーターは、2つのディレクティブを使用してGo定数をPHPにエクスポートすることをサポートしていますグローバル定数用の`//export_php:const`とクラス定数用の`//export_php:classconst`です。これにより、GoとPHPコード間で設定値、ステータスコード、その他の定数を共有できます。
#### グローバル定数
@@ -240,25 +240,25 @@ const STATUS_ERROR = iota
#### クラス定数
`//export_php:classconstant ClassName`ディレクティブを使用して、特定のPHPクラスに属する定数を作成できます
`//export_php:classconst ClassName`ディレクティブを使用して、特定のPHPクラスに属する定数を作成できます
```go
//export_php:classconstant User
//export_php:classconst User
const STATUS_ACTIVE = 1
//export_php:classconstant User
//export_php:classconst User
const STATUS_INACTIVE = 0
//export_php:classconstant User
//export_php:classconst User
const ROLE_ADMIN = "admin"
//export_php:classconstant Order
//export_php:classconst Order
const STATE_PENDING = iota
//export_php:classconstant Order
//export_php:classconst Order
const STATE_PROCESSING = iota
//export_php:classconstant Order
//export_php:classconst Order
const STATE_COMPLETED = iota
```
@@ -294,10 +294,10 @@ const STR_REVERSE = iota
//export_php:const
const STR_NORMAL = iota
//export_php:classconstant StringProcessor
//export_php:classconst StringProcessor
const MODE_LOWERCASE = 1
//export_php:classconstant StringProcessor
//export_php:classconst StringProcessor
const MODE_UPPERCASE = 2
//export_php:function repeat_this(string $str, int $count, int $mode): string

View File

@@ -88,7 +88,8 @@ LaravelアプリをLinux用のスタンドアロンバイナリとしてパッ
1. アプリのリポジトリに`static-build.Dockerfile`という名前のファイルを作成します:
```dockerfile
FROM --platform=linux/amd64 dunglas/frankenphp:static-builder
FROM --platform=linux/amd64 dunglas/frankenphp:static-builder-gnu
# バイナリをmusl-libcシステムで実行する場合は、static-builder-musl を使用してください
# アプリをコピー
WORKDIR /go/src/app/dist/app

View File

@@ -90,7 +90,8 @@ Follow these steps to package your Laravel app as a standalone binary for Linux:
1. Create a file named `static-build.Dockerfile` in the repository of your app:
```dockerfile
FROM --platform=linux/amd64 dunglas/frankenphp:static-builder
FROM --platform=linux/amd64 dunglas/frankenphp:static-builder-gnu
# If you intend to run the binary on musl-libc systems, use static-builder-musl instead
# Copy your app
WORKDIR /go/src/app/dist/app

View File

@@ -54,7 +54,30 @@ To subscribe to updates, use the native [`EventSource`](https://developer.mozill
## Publishing Updates
### Using `file_put_contents()`
### Using `mercure_publish()`
FrankenPHP provides a convenient `mercure_publish()` function to publish updates to the built-in Mercure hub:
```php
<?php
// public/publish.php
$updateID = mercure_publish('my-topic', json_encode(['key' => 'value']));
// Write to FrankenPHP's logs
error_log("update $updateID published", 4);
```
The full function signature is:
```php
/**
* @param string|string[] $topics
*/
function mercure_publish(string|array $topics, string $data = '', bool $private = false, ?string $id = null, ?string $type = null, ?int $retry = null): string {}
```
### Using `file_get_contents()`
To dispatch an update to connected subscribers, send an authenticated POST request to the Mercure hub with the `topic` and `data` parameters:
@@ -64,7 +87,7 @@ To dispatch an update to connected subscribers, send an authenticated POST reque
const JWT = 'eyJhbGciOiJIUzI1NiJ9.eyJtZXJjdXJlIjp7InB1Ymxpc2giOlsiKiJdfX0.PXwpfIGng6KObfZlcOXvcnWCJOWTFLtswGI5DZuWSK4';
$updateID = file_put_contents('https://localhost/.well-known/mercure', context: stream_context_create(['http' => [
$updateID = file_get_contents('https://localhost/.well-known/mercure', context: stream_context_create(['http' => [
'method' => 'POST',
'header' => "Content-type: application/x-www-form-urlencoded\r\nAuthorization: Bearer " . JWT,
'content' => http_build_query([

View File

@@ -41,11 +41,9 @@ especially when compiled in ZTS mode (thread-safe), which is required for Franke
Also, [some bugs only happen when using musl](https://github.com/php/php-src/issues?q=sort%3Aupdated-desc+is%3Aissue+is%3Aopen+label%3ABug+musl).
In production environments, we recommend using FrankenPHP linked against glibc.
In production environments, we recommend using FrankenPHP linked against glibc, compiled with an appropriate optimization level.
This can be achieved by using the Debian Docker images (the default), downloading the -gnu suffix binary from our [Releases](https://github.com/php/frankenphp/releases), or by [compiling FrankenPHP from sources](compile.md).
Alternatively, we provide static musl binaries compiled with [the mimalloc allocator](https://github.com/microsoft/mimalloc), which alleviates the problems in threaded scenarios.
This can be achieved by using the Debian Docker images, using our maintainers [.deb](https://debs.henderkes.com) or [.rpm](https://rpms.henderkes.com) packages, or by [compiling FrankenPHP from sources](compile.md).
## Go Runtime Configuration

View File

@@ -24,22 +24,64 @@ conjunto de slides:
## Começando
### Binário independente
Fornecemos binários estáticos do FrankenPHP para Linux e macOS contendo o
[PHP 8.4](https://www.php.net/releases/8.4/pt_BR.php) e as extensões PHP mais
populares.
No Windows, use [WSL](https://learn.microsoft.com/pt-br/windows/wsl/) para
executar o FrankenPHP.
[Baixe o FrankenPHP](https://github.com/php/frankenphp/releases) ou copie esta
linha no seu terminal para instalar automaticamente a versão apropriada para sua
plataforma:
### Script de instalação
Você pode copiar esta linha no seu terminal para instalar automaticamente a
versão apropriada para sua plataforma:
```console
curl https://frankenphp.dev/install.sh | sh
mv frankenphp /usr/local/bin/
```
### Binário independente
Fornecemos binários estáticos do FrankenPHP para desenvolvimento em Linux e macOS contendo o
[PHP 8.4](https://www.php.net/releases/8.4/pt_BR.php) e as extensões PHP mais populares.
[Baixe o FrankenPHP](https://github.com/php/frankenphp/releases)
**Instalação de extensões:** As extensões mais comuns já estão incluídas. Não é possível instalar mais extensões.
### Pacotes rpm
Nossos mantenedores oferecem pacotes rpm para todos os sistemas que usam `dnf`. Para instalar, execute:
```console
sudo dnf install https://rpm.henderkes.com/static-php-1-0.noarch.rpm
sudo dnf module enable php-zts:static-8.4 # 8.2-8.5 disponíveis
sudo dnf install frankenphp
```
**Instalação de extensões:** `sudo dnf install php-zts-<extension>`
Para extensões não disponíveis por padrão, use o [PIE](https://github.com/php/pie):
```console
sudo dnf install pie-zts
sudo pie-zts install asgrim/example-pie-extension
```
### Pacotes deb
Nossos mantenedores oferecem pacotes deb para todos os sistemas que usam `apt`. Para instalar, execute:
```console
sudo curl -fsSL https://key.henderkes.com/static-php.gpg -o /usr/share/keyrings/static-php.gpg && \
echo "deb [signed-by=/usr/share/keyrings/static-php.gpg] https://deb.henderkes.com/ stable main" | sudo tee /etc/apt/sources.list.d/static-php.list && \
sudo apt update
sudo apt install frankenphp
```
**Instalação de extensões:** `sudo apt install php-zts-<extension>`
Para extensões não disponíveis por padrão, use o [PIE](https://github.com/php/pie):
```console
sudo apt install pie-zts
sudo pie-zts install asgrim/example-pie-extension
```
Para servir o conteúdo do diretório atual, execute:
@@ -85,12 +127,28 @@ Para instalá-lo:
brew install dunglas/frankenphp/frankenphp
```
**Instalação de extensões:** Use o [PIE](https://github.com/php/pie).
### Uso
Para servir o conteúdo do diretório atual, execute:
```console
frankenphp php-server
```
Você também pode executar scripts de linha de comando com:
```console
frankenphp php-cli /caminho/para/seu/script.php
```
Para os pacotes deb e rpm, você também pode iniciar o serviço systemd:
```console
sudo systemctl start frankenphp
```
## Documentação
- [Modo clássico](classic.md)

View File

@@ -6,7 +6,7 @@ Variantes do Debian e do Alpine Linux são fornecidas para arquiteturas
populares.
Variantes do Debian são recomendadas.
Variantes para PHP 8.2, 8.3 e 8.4 são fornecidas.
Variantes para PHP 8.2, 8.3, 8.4 e 8.5 são fornecidas.
As tags seguem este padrão:
`dunglas/frankenphp:<versao-do-frankenphp>-php<versao-do-php>-<so>`.

View File

@@ -66,7 +66,8 @@ Docker que fornecemos.
aplicação:
```dockerfile
FROM --platform=linux/amd64 dunglas/frankenphp:static-builder
FROM --platform=linux/amd64 dunglas/frankenphp:static-builder-gnu
# Se você pretende executar o binário em sistemas musl-libc, use o static-builder-musl
# Copia sua aplicação
WORKDIR /go/src/app/dist/app

View File

@@ -412,7 +412,7 @@ segurança de tipos.
### Declarando constantes
O gerador suporta a exportação de constantes Go para PHP usando duas diretivas:
`//export_php:const` para constantes globais e `//export_php:classconstant` para
`//export_php:const` para constantes globais e `//export_php:classconst` para
constantes de classe.
Isso permite que você compartilhe valores de configuração, códigos de status e
outras constantes entre código Go e PHP.
@@ -437,26 +437,26 @@ const STATUS_ERROR = iota
#### Constantes de classe
Use a diretiva `//export_php:classconstant ClassName` para criar constantes que
Use a diretiva `//export_php:classconst ClassName` para criar constantes que
pertencem a uma classe PHP específica:
```go
//export_php:classconstant User
//export_php:classconst User
const STATUS_ACTIVE = 1
//export_php:classconstant User
//export_php:classconst User
const STATUS_INACTIVE = 0
//export_php:classconstant User
//export_php:classconst User
const ROLE_ADMIN = "admin"
//export_php:classconstant Order
//export_php:classconst Order
const STATE_PENDING = iota
//export_php:classconstant Order
//export_php:classconst Order
const STATE_PROCESSING = iota
//export_php:classconstant Order
//export_php:classconst Order
const STATE_COMPLETED = iota
```
@@ -502,10 +502,10 @@ const STR_REVERSE = iota
//export_php:const
const STR_NORMAL = iota
//export_php:classconstant StringProcessor
//export_php:classconst StringProcessor
const MODE_LOWERCASE = 1
//export_php:classconstant StringProcessor
//export_php:classconst StringProcessor
const MODE_UPPERCASE = 2
//export_php:function repeat_this(string $str, int $count, int $mode): string

View File

@@ -108,7 +108,8 @@ independente para Linux:
aplicação:
```dockerfile
FROM --platform=linux/amd64 dunglas/frankenphp:static-builder
FROM --platform=linux/amd64 dunglas/frankenphp:static-builder-gnu
# Se você pretende executar o binário em sistemas musl-libc, use o static-builder-musl
# Copia sua aplicação
WORKDIR /go/src/app/dist/app

View File

@@ -16,6 +16,63 @@ FrankenPHP также может использоваться как автон
## Начало работы
В Windows используйте [WSL](https://learn.microsoft.com/windows/wsl/) для запуска FrankenPHP.
### Скрипт установки
Скопируйте и выполните эту команду в терминале, чтобы автоматически установить подходящую версию для вашей платформы:
```console
curl https://frankenphp.dev/install.sh | sh
```
### Автономный бинарный файл
Если вы предпочитаете не использовать Docker, мы предоставляем автономные статические бинарные файлы FrankenPHP для Linux и macOS, включающие [PHP 8.4](https://www.php.net/releases/8.4/en.php) и большинство популярных PHPрасширений.
[Скачать FrankenPHP](https://github.com/php/frankenphp/releases)
**Установка расширений:** Наиболее распространенные расширения уже включены. Устанавливать дополнительные расширения невозможно.
### Пакеты rpm
Наши мейнтейнеры предлагают rpmпакеты для всех систем с `dnf`. Для установки выполните:
```console
sudo dnf install https://rpm.henderkes.com/static-php-1-0.noarch.rpm
sudo dnf module enable php-zts:static-8.4 # доступны 8.28.5
sudo dnf install frankenphp
```
**Установка расширений:** `sudo dnf install php-zts-<extension>`
Для расширений, недоступных по умолчанию, используйте [PIE](https://github.com/php/pie):
```console
sudo dnf install pie-zts
sudo pie-zts install asgrim/example-pie-extension
```
### Пакеты deb
Наши мейнтейнеры предлагают debпакеты для всех систем с `apt`. Для установки выполните:
```console
sudo curl -fsSL https://key.henderkes.com/static-php.gpg -o /usr/share/keyrings/static-php.gpg && \
echo "deb [signed-by=/usr/share/keyrings/static-php.gpg] https://deb.henderkes.com/ stable main" | sudo tee /etc/apt/sources.list.d/static-php.list && \
sudo apt update
sudo apt install frankenphp
```
**Установка расширений:** `sudo apt install php-zts-<extension>`
Для расширений, недоступных по умолчанию, используйте [PIE](https://github.com/php/pie):
```console
sudo apt install pie-zts
sudo pie-zts install asgrim/example-pie-extension
```
### Docker
```console
@@ -31,31 +88,36 @@ docker run -v .:/app/public \
> Не используйте `https://127.0.0.1`. Используйте `https://localhost` и настройте самоподписанный сертификат.
> Чтобы изменить используемый домен, настройте переменную окружения [`SERVER_NAME`](config.md#переменные-окружения).
### Автономный бинарный файл
### Homebrew
Если вы предпочитаете не использовать Docker, мы предоставляем автономный бинарный файл FrankenPHP для Linux и macOS, включающий [PHP 8.4](https://www.php.net/releases/8.4/en.php) и большинство популярных PHP-расширений.
Для Windows используйте [WSL](https://learn.microsoft.com/windows/wsl/) для запуска FrankenPHP.
[Скачать FrankenPHP](https://github.com/php/frankenphp/releases) или выполните следующую команду для автоматической установки подходящей версии:
FrankenPHP также доступен как пакет [Homebrew](https://brew.sh) для macOS и Linux.
```console
curl https://frankenphp.dev/install.sh | sh
mv frankenphp /usr/local/bin/
brew install dunglas/frankenphp/frankenphp
```
Для запуска содержимого текущей директории выполните:
**Установка расширений:** Используйте [PIE](https://github.com/php/pie).
### Использование
Для запуска содержимого текущего каталога выполните:
```console
frankenphp php-server
```
Вы также можете запускать CLI-скрипты:
Также можно запускать CLIскрипты:
```console
frankenphp php-cli /path/to/your/script.php
```
Для пакетов deb и rpm можно запустить сервис systemd:
```console
sudo systemctl start frankenphp
```
## Документация
- [Worker режим](https://frankenphp.dev/docs/worker/)

View File

@@ -2,7 +2,7 @@
[Docker-образы FrankenPHP](https://hub.docker.com/r/dunglas/frankenphp) основаны на [официальных PHP-образах](https://hub.docker.com/_/php/). Доступны варианты для Debian и Alpine Linux для популярных архитектур. Рекомендуется использовать Debian-варианты.
Доступны версии для PHP 8.2, 8.3 и 8.4.
Доступны версии для PHP 8.2, 8.3, 8.4 и 8.5.
Теги следуют следующему шаблону: `dunglas/frankenphp:<frankenphp-version>-php<php-version>-<os>`.

View File

@@ -52,7 +52,8 @@ composer dump-env prod
1. Создайте файл `static-build.Dockerfile` в репозитории вашего приложения:
```dockerfile
FROM --platform=linux/amd64 dunglas/frankenphp:static-builder
FROM --platform=linux/amd64 dunglas/frankenphp:static-builder-gnu
# Если вы планируете запускать бинарный файл на системах с musl-libc, используйте static-builder-musl
# Скопировать приложение
WORKDIR /go/src/app/dist/app

View File

@@ -85,7 +85,8 @@ php artisan octane:frankenphp
1. Создайте файл с именем `static-build.Dockerfile` в репозитории вашего приложения:
```dockerfile
FROM --platform=linux/amd64 dunglas/frankenphp:static-builder
FROM --platform=linux/amd64 dunglas/frankenphp:static-builder-gnu
# Если вы планируете запускать бинарный файл на системах с musl-libc, используйте static-builder-musl
# Скопируйте ваше приложение
WORKDIR /go/src/app/dist/app

View File

@@ -16,6 +16,64 @@ FrankenPHP, PHP'yi `net/http` kullanarak herhangi bir uygulamaya yerleştirmek i
## Başlarken
Windows üzerinde FrankenPHP çalıştırmak için [WSL](https://learn.microsoft.com/windows/wsl/) kullanın.
### Kurulum Betiği
Platformunuza uygun sürümü otomatik olarak kurmak için bu satırı terminalinize kopyalayabilirsiniz:
```console
curl https://frankenphp.dev/install.sh | sh
```
### Binary Çıktısı
Docker kullanmayı tercih etmiyorsanız, Linux ve macOS için geliştirme amaçlı bağımsız (statik) FrankenPHP binary dosyaları sağlıyoruz;
[PHP 8.4](https://www.php.net/releases/8.4/en.php) ve en popüler PHP eklentilerinin çoğu dahildir.
[FrankenPHP'yi indirin](https://github.com/php/frankenphp/releases)
**Eklenti kurulumu:** Yaygın eklentiler paketle birlikte gelir. Daha fazla eklenti yüklemek mümkün değildir.
### rpm Paketleri
Bakımcılarımız `dnf` kullanan tüm sistemler için rpm paketleri sunuyor. Kurulum için:
```console
sudo dnf install https://rpm.henderkes.com/static-php-1-0.noarch.rpm
sudo dnf module enable php-zts:static-8.4 # 8.2-8.5 mevcut
sudo dnf install frankenphp
```
**Eklenti kurulumu:** `sudo dnf install php-zts-<extension>`
Varsayılan olarak mevcut olmayan eklentiler için [PIE](https://github.com/php/pie) kullanın:
```console
sudo dnf install pie-zts
sudo pie-zts install asgrim/example-pie-extension
```
### deb Paketleri
Bakımcılarımız `apt` kullanan tüm sistemler için deb paketleri sunuyor. Kurulum için:
```console
sudo curl -fsSL https://key.henderkes.com/static-php.gpg -o /usr/share/keyrings/static-php.gpg && \
echo "deb [signed-by=/usr/share/keyrings/static-php.gpg] https://deb.henderkes.com/ stable main" | sudo tee /etc/apt/sources.list.d/static-php.list && \
sudo apt update
sudo apt install frankenphp
```
**Eklenti kurulumu:** `sudo apt install php-zts-<extension>`
Varsayılan olarak mevcut olmayan eklentiler için [PIE](https://github.com/php/pie) kullanın:
```console
sudo apt install pie-zts
sudo pie-zts install asgrim/example-pie-extension
```
### Docker
```console
@@ -31,23 +89,36 @@ docker run -v $PWD:/app/public \
> `https://127.0.0.1` kullanmaya çalışmayın. `https://localhost` kullanın ve kendinden imzalı sertifikayı kabul edin.
> Kullanılacak alan adını değiştirmek için [`SERVER_NAME` ortam değişkenini](https://frankenphp.dev/tr/docs/config#ortam-değişkenleri) kullanın.
### Binary Çıktısı
### Homebrew
Docker kullanmayı tercih etmiyorsanız, Linux ve macOS için bağımsız FrankenPHP binary dosyası sağlıyoruz
[PHP 8.4](https://www.php.net/releases/8.4/en.php) ve en popüler PHP eklentilerini de içermekte: [FrankenPHP](https://github.com/php/frankenphp/releases) indirin
FrankenPHP, macOS ve Linux için [Homebrew](https://brew.sh) paketi olarak da mevcuttur.
Geçerli dizinin içeriğini başlatmak için çalıştırın:
```console
brew install dunglas/frankenphp/frankenphp
```
**Eklenti kurulumu:** [PIE](https://github.com/php/pie) kullanın.
### Kullanım
Geçerli dizinin içeriğini sunmak için çalıştırın:
```console
frankenphp php-server
```
Ayrıca aşağıdaki tek komut satırı ile de çalıştırabilirsiniz:
Komut satırı betiklerini şu şekilde çalıştırabilirsiniz:
```console
frankenphp php-cli /path/to/your/script.php
```
deb ve rpm paketleri için systemd servisini de başlatabilirsiniz:
```console
sudo systemctl start frankenphp
```
## Docs
- [Worker modu](worker.md)

View File

@@ -2,7 +2,7 @@
[Resmi PHP imajları](https://hub.docker.com/_/php/) temel alınarak [FrankenPHP Docker imajları](https://hub.docker.com/r/dunglas/frankenphp) hazırlanmıştır. Popüler mimariler için Debian ve Alpine Linux varyantları sağlanmıştır. Debian dağıtımı tavsiye edilir.
PHP 8.2, 8.3 ve 8.4 için varyantlar sağlanmıştır. [Etiketlere göz atın](https://hub.docker.com/r/dunglas/frankenphp/tags).
PHP 8.2, 8.3, 8.4 ve 8.5 için varyantlar sağlanmıştır. [Etiketlere göz atın](https://hub.docker.com/r/dunglas/frankenphp/tags).
## İmajlar Nasıl Kullanılır

View File

@@ -46,7 +46,8 @@ Bir Linux binary çıktısı almanın en kolay yolu, sağladığımız Docker ta
1. Hazırladığınız uygulamanın deposunda `static-build.Dockerfile` adlı bir dosya oluşturun:
```dockerfile
FROM --platform=linux/amd64 dunglas/frankenphp:static-builder
FROM --platform=linux/amd64 dunglas/frankenphp:static-builder-gnu
# İkili dosyayı musl-libc sistemlerinde çalıştırmayı düşünüyorsanız static-builder-musl kullanın
# Uygulamanızı kopyalayın
WORKDIR /go/src/app/dist/app
@@ -55,7 +56,6 @@ Bir Linux binary çıktısı almanın en kolay yolu, sağladığımız Docker ta
# Statik binary dosyasını oluşturun, yalnızca istediğiniz PHP eklentilerini seçtiğinizden emin olun
WORKDIR /go/src/app/
RUN EMBED=dist/app/ \
PHP_EXTENSIONS=ctype,iconv,pdo_sqlite \
./build-static.sh
```
@@ -86,7 +86,6 @@ Docker kullanmak istemiyorsanız veya bir macOS binary dosyası oluşturmak isti
git clone https://github.com/php/frankenphp
cd frankenphp
EMBED=/path/to/your/app \
PHP_EXTENSIONS=ctype,iconv,pdo_sqlite \
./build-static.sh
```

View File

@@ -10,13 +10,13 @@ Bu komutu Laravel uygulamanızın ana dizininden çalıştırın:
docker run -p 80:80 -p 443:443 -p 443:443/udp -v $PWD:/app dunglas/frankenphp
```
And tadını çıkarın!
Ve tadını çıkarın!
## Yerel Kurulum
Alternatif olarak, Laravel projelerinizi FrankenPHP ile yerel makinenizden çalıştırabilirsiniz:
1. [Sisteminize karşılık gelen binary dosyayı indirin](https://github.com/php/frankenphp/releases)
1. [Sisteminize karşılık gelen ikili dosyayı indirin](../#standalone-binary)
2. Aşağıdaki yapılandırmayı Laravel projenizin kök dizinindeki `Caddyfile` adlı bir dosyaya ekleyin:
```caddyfile
@@ -30,8 +30,10 @@ Alternatif olarak, Laravel projelerinizi FrankenPHP ile yerel makinenizden çal
root public/
# Sıkıştırmayı etkinleştir (isteğe bağlı)
encode zstd br gzip
# PHP dosyalarını public/ dizininden çalıştırın ve varlıkları sunun
php_server
# public/ dizininden PHP dosyalarını çalıştırın ve statik dosyaları servis edin
php_server {
try_files {path} index.php
}
}
```
@@ -64,11 +66,118 @@ php artisan octane:frankenphp
- `--admin-port`: Yönetici sunucusunun erişilebilir olması gereken port (varsayılan: `2019`)
- `--workers`: İstekleri işlemek için hazır olması gereken worker sayısı (varsayılan: `auto`)
- `--max-requests`: Sunucu yeniden yüklenmeden önce işlenecek istek sayısı (varsayılan: `500`)
- `--caddyfile`: FrankenPHP `Caddyfile` dosyasının yolu
- `--caddyfile`: FrankenPHP `Caddyfile` dosyasının yolu (varsayılan: [Laravel Octane içinde bulunan şablon `Caddyfile`](https://github.com/laravel/octane/blob/2.x/src/Commands/stubs/Caddyfile))
- `--https`: HTTPS, HTTP/2 ve HTTP/3'ü etkinleştirin ve sertifikaları otomatik olarak oluşturup yenileyin
- `--http-redirect`: HTTP'den HTTPS'ye yeniden yönlendirmeyi etkinleştir (yalnızca --https geçilirse etkinleştirilir)
- `--watch`: Uygulamada kod değişikliği olduğunda sunucuyu otomatik olarak yeniden yükle
- `--http-redirect`: HTTP'den HTTPS'ye yeniden yönlendirmeyi etkinleştir (yalnızca --https ile birlikte geçilirse etkinleşir)
- `--watch`: Uygulama değiştirildiğinde sunucuyu otomatik olarak yeniden yükle
- `--poll`: Dosyaları bir ağ üzerinden izlemek için izleme sırasında dosya sistemi yoklamasını kullanın
- `--log-level`: Belirtilen günlük seviyesinde veya üzerinde günlük mesajları
- `--log-level`: Yerel Caddy günlüğünü kullanarak belirtilen günlük seviyesinde veya üzerinde mesajları kaydedin
Laravel Octane hakkında daha fazla bilgi edinmek için [Laravel Octane resmi belgelerine](https://laravel.com/docs/octane) göz atın.
> [!TIP]
> Yapılandırılmış JSON günlükleri elde etmek için (log analitik çözümleri kullanırken faydalıdır), `--log-level` seçeneğini açıkça geçin.
[Laravel Octane hakkında daha fazla bilgiyi resmi belgelerde bulabilirsiniz](https://laravel.com/docs/octane).
## Laravel Uygulamalarını Bağımsız Çalıştırılabilir Dosyalar Olarak Dağıtma
[FrankenPHP'nin uygulama gömme özelliğini](embed.md) kullanarak, Laravel
uygulamalarını bağımsız çalıştırılabilir dosyalar olarak dağıtmak mümkündür.
Linux için Laravel uygulamanızı bağımsız bir çalıştırılabilir olarak paketlemek için şu adımları izleyin:
1. Uygulamanızın deposunda `static-build.Dockerfile` adında bir dosya oluşturun:
```dockerfile
FROM --platform=linux/amd64 dunglas/frankenphp:static-builder-gnu
# İkiliyi musl-libc sistemlerinde çalıştırmayı düşünüyorsanız, bunun yerine static-builder-musl kullanın
# Uygulamanızı kopyalayın
WORKDIR /go/src/app/dist/app
COPY . .
# Yer kaplamamak için testleri ve diğer gereksiz dosyaları kaldırın
# Alternatif olarak, bu dosyaları bir .dockerignore dosyasına ekleyin
RUN rm -Rf tests/
# .env dosyasını kopyalayın
RUN cp .env.example .env
# APP_ENV ve APP_DEBUG değerlerini production için uygun hale getirin
RUN sed -i'' -e 's/^APP_ENV=.*/APP_ENV=production/' -e 's/^APP_DEBUG=.*/APP_DEBUG=false/' .env
# Gerekirse .env dosyanıza diğer değişiklikleri yapın
# Bağımlılıkları yükleyin
RUN composer install --ignore-platform-reqs --no-dev -a
# Statik ikiliyi derleyin
WORKDIR /go/src/app/
RUN EMBED=dist/app/ ./build-static.sh
```
> [!CAUTION]
> Bazı `.dockerignore` dosyaları
> `vendor/` dizinini ve `.env` dosyalarını yok sayar. Derlemeden önce `.dockerignore` dosyasını buna göre ayarladığınızdan veya kaldırdığınızdan emin olun.
2. İmajı oluşturun:
```console
docker build -t static-laravel-app -f static-build.Dockerfile .
```
3. İkili dosyayı dışa aktarın:
```console
docker cp $(docker create --name static-laravel-app-tmp static-laravel-app):/go/src/app/dist/frankenphp-linux-x86_64 frankenphp ; docker rm static-laravel-app-tmp
```
4. Önbellekleri doldurun:
```console
frankenphp php-cli artisan optimize
```
5. Veritabanı migration'larını çalıştırın (varsa):
```console
frankenphp php-cli artisan migrate
```
6. Uygulamanın gizli anahtarını oluşturun:
```console
frankenphp php-cli artisan key:generate
```
7. Sunucuyu başlatın:
```console
frankenphp php-server
```
Uygulamanız artık hazır!
Mevcut seçenekler hakkında daha fazla bilgi edinin ve diğer işletim sistemleri için nasıl ikili derleneceğini [uygulama gömme](embed.md)
belgelerinde öğrenin.
### Depolama Yolunu Değiştirme
Varsayılan olarak, Laravel yüklenen dosyaları, önbellekleri, logları vb. uygulamanın `storage/` dizininde saklar.
Gömülü uygulamalar için bu uygun değildir, çünkü her yeni sürüm farklı bir geçici dizine çıkarılacaktır.
Geçici dizin dışında bir dizin kullanmak için `LARAVEL_STORAGE_PATH` ortam değişkenini ayarlayın (örneğin, `.env` dosyanızda) veya `Illuminate\Foundation\Application::useStoragePath()` metodunu çağırın.
### Bağımsız Çalıştırılabilir Dosyalarla Octane'i Çalıştırma
Laravel Octane uygulamalarını bağımsız çalıştırılabilir dosyalar olarak paketlemek bile mümkündür!
Bunu yapmak için, [Octane'i doğru şekilde kurun](#laravel-octane) ve [önceki bölümde](#laravel-uygulamalarını-bağımsız-çalıştırılabilir-dosyalar-olarak-dağıtma) açıklanan adımları izleyin.
Ardından, Octane üzerinden FrankenPHP'yi worker modunda başlatmak için şunu çalıştırın:
```console
PATH="$PWD:$PATH" frankenphp php-cli artisan octane:frankenphp
```
> [!CAUTION]
> Komutun çalışması için, bağımsız ikili dosya mutlaka `frankenphp` olarak adlandırılmış olmalıdır,
> çünkü Octane, yol üzerinde `frankenphp` adlı bir programın mevcut olmasını bekler.

View File

@@ -37,6 +37,9 @@ frankenphp php-server --worker /path/to/your/worker/script.php --watch="/path/to
## Symfony Runtime
> [!TIP]
> The following section is only necessary prior to Symfony 7.4, where native support for FrankenPHP worker mode was introduced.
The worker mode of FrankenPHP is supported by the [Symfony Runtime Component](https://symfony.com/doc/current/components/runtime.html).
To start any Symfony application in a worker, install the FrankenPHP package of [PHP Runtime](https://github.com/php-runtime/runtime):

5
env.go
View File

@@ -1,10 +1,9 @@
package frankenphp
// #cgo nocallback frankenphp_init_persistent_string
// #cgo nocallback frankenphp_add_assoc_str_ex
// #cgo noescape frankenphp_init_persistent_string
// #cgo noescape frankenphp_add_assoc_str_ex
// #include "frankenphp.h"
// #include <Zend/zend_API.h>
import "C"
import (
"os"
@@ -98,7 +97,7 @@ func go_getfullenv(threadIndex C.uintptr_t, trackVarsArray *C.zval) {
env := getSandboxedEnv(thread)
for key, val := range env {
C.frankenphp_add_assoc_str_ex(trackVarsArray, toUnsafeChar(key), C.size_t(len(key)), val)
C.add_assoc_str_ex(trackVarsArray, toUnsafeChar(key), C.size_t(len(key)), val)
}
}

View File

@@ -51,7 +51,6 @@ frankenphp_version frankenphp_get_version() {
frankenphp_config frankenphp_get_config() {
return (frankenphp_config){
frankenphp_get_version(),
#ifdef ZTS
true,
#else
@@ -75,6 +74,10 @@ __thread uintptr_t thread_index;
__thread bool is_worker_thread = false;
__thread zval *os_environment = NULL;
void frankenphp_update_local_thread_context(bool is_worker) {
is_worker_thread = is_worker;
}
static void frankenphp_update_request_context() {
/* the server context is stored on the go side, still SG(server_context) needs
* to not be NULL */
@@ -82,7 +85,7 @@ static void frankenphp_update_request_context() {
/* status It is not reset by zend engine, set it to 200. */
SG(sapi_headers).http_response_code = 200;
is_worker_thread = go_update_request_info(thread_index, &SG(request_info));
go_update_request_info(thread_index, &SG(request_info));
}
static void frankenphp_free_request_context() {
@@ -206,11 +209,6 @@ PHPAPI void get_full_env(zval *track_vars_array) {
go_getfullenv(thread_index, track_vars_array);
}
void frankenphp_add_assoc_str_ex(zval *track_vars_array, char *key,
size_t keylen, zend_string *val) {
add_assoc_str_ex(track_vars_array, key, keylen, val);
}
/* Adapted from php_request_startup() */
static int frankenphp_worker_request_startup() {
int retval = SUCCESS;
@@ -464,12 +462,16 @@ PHP_FUNCTION(frankenphp_handle_request) {
/*
* If an exception occurred, print the message to the client before
* closing the connection and bailout.
* closing the connection.
*/
if (EG(exception) && !zend_is_unwind_exit(EG(exception)) &&
!zend_is_graceful_exit(EG(exception))) {
zend_exception_error(EG(exception), E_ERROR);
zend_bailout();
if (EG(exception)) {
if (!zend_is_unwind_exit(EG(exception)) &&
!zend_is_graceful_exit(EG(exception))) {
zend_exception_error(EG(exception), E_ERROR);
} else {
/* exit() will jump directly to after php_execute_script */
zend_bailout();
}
}
frankenphp_worker_request_shutdown();
@@ -505,7 +507,72 @@ PHP_FUNCTION(headers_send) {
RETURN_LONG(sapi_send_headers());
}
PHP_FUNCTION(mercure_publish) {
zval *topics;
zend_string *data = NULL, *id = NULL, *type = NULL;
zend_bool private = 0;
zend_long retry = 0;
bool retry_is_null = 1;
ZEND_PARSE_PARAMETERS_START(1, 6)
Z_PARAM_ZVAL(topics)
Z_PARAM_OPTIONAL
Z_PARAM_STR_OR_NULL(data)
Z_PARAM_BOOL(private)
Z_PARAM_STR_OR_NULL(id)
Z_PARAM_STR_OR_NULL(type)
Z_PARAM_LONG_OR_NULL(retry, retry_is_null)
ZEND_PARSE_PARAMETERS_END();
if (Z_TYPE_P(topics) != IS_ARRAY && Z_TYPE_P(topics) != IS_STRING) {
zend_argument_type_error(1, "must be of type array|string");
RETURN_THROWS();
}
struct go_mercure_publish_return result =
go_mercure_publish(thread_index, topics, data, private, id, type, retry);
switch (result.r1) {
case 0:
RETURN_STR(result.r0);
case 1:
zend_throw_exception(spl_ce_RuntimeException, "No Mercure hub configured",
0);
RETURN_THROWS();
case 2:
zend_throw_exception(spl_ce_RuntimeException, "Publish failed", 0);
RETURN_THROWS();
}
zend_throw_exception(spl_ce_RuntimeException,
"FrankenPHP not built with Mercure support", 0);
RETURN_THROWS();
}
PHP_FUNCTION(frankenphp_log) {
zend_string *message = NULL;
zend_long level = 0;
zval *context = NULL;
ZEND_PARSE_PARAMETERS_START(1, 3)
Z_PARAM_STR(message)
Z_PARAM_OPTIONAL
Z_PARAM_LONG(level)
Z_PARAM_ARRAY(context)
ZEND_PARSE_PARAMETERS_END();
char *ret = NULL;
ret = go_log_attrs(thread_index, message, level, context);
if (ret != NULL) {
zend_throw_exception(spl_ce_RuntimeException, ret, 0);
free(ret);
RETURN_THROWS();
}
}
PHP_MINIT_FUNCTION(frankenphp) {
register_frankenphp_symbols(module_number);
zend_function *func;
// Override putenv
@@ -606,8 +673,9 @@ static char *frankenphp_read_cookies(void) {
}
/* all variables with well defined keys can safely be registered like this */
void frankenphp_register_trusted_var(zend_string *z_key, char *value,
size_t val_len, HashTable *ht) {
static inline void frankenphp_register_trusted_var(zend_string *z_key,
char *value, size_t val_len,
HashTable *ht) {
if (value == NULL) {
zval empty;
ZVAL_EMPTY_STRING(&empty);
@@ -798,7 +866,7 @@ static void frankenphp_register_variables(zval *track_vars_array) {
}
static void frankenphp_log_message(const char *message, int syslog_type_int) {
go_log((char *)message, syslog_type_int);
go_log(thread_index, (char *)message, syslog_type_int);
}
static char *frankenphp_getenv(const char *name, size_t name_len) {

View File

@@ -41,55 +41,71 @@ import (
type contextKeyStruct struct{}
var contextKey = contextKeyStruct{}
var (
ErrInvalidRequest = errors.New("not a FrankenPHP request")
ErrAlreadyStarted = errors.New("FrankenPHP is already started")
ErrInvalidPHPVersion = errors.New("FrankenPHP is only compatible with PHP 8.2+")
ErrMainThreadCreation = errors.New("error creating the main thread")
ErrRequestContextCreation = errors.New("error during request context creation")
ErrScriptExecution = errors.New("error during PHP script execution")
ErrNotRunning = errors.New("FrankenPHP is not running. For proper configuration visit: https://frankenphp.dev/docs/config/#caddyfile-config")
ErrInvalidRequest = errors.New("not a FrankenPHP request")
ErrAlreadyStarted = errors.New("FrankenPHP is already started")
ErrInvalidPHPVersion = errors.New("FrankenPHP is only compatible with PHP 8.2+")
ErrMainThreadCreation = errors.New("error creating the main thread")
ErrScriptExecution = errors.New("error during PHP script execution")
ErrNotRunning = errors.New("FrankenPHP is not running. For proper configuration visit: https://frankenphp.dev/docs/config/#caddyfile-config")
isRunning bool
ErrInvalidRequestPath = ErrRejected{"invalid request path", http.StatusBadRequest}
ErrInvalidContentLengthHeader = ErrRejected{"invalid Content-Length header", http.StatusBadRequest}
ErrMaxWaitTimeExceeded = ErrRejected{"maximum request handling time exceeded", http.StatusServiceUnavailable}
loggerMu sync.RWMutex
logger *slog.Logger
contextKey = contextKeyStruct{}
serverHeader = []string{"FrankenPHP"}
isRunning bool
onServerShutdown []func()
// Set default values to make Shutdown() idempotent
globalMu sync.Mutex
globalCtx = context.Background()
globalLogger = slog.Default()
metrics Metrics = nullMetrics{}
maxWaitTime time.Duration
)
type ErrRejected struct {
message string
status int
}
func (e ErrRejected) Error() string {
return e.message
}
type syslogLevel int
const (
emerg syslogLevel = iota // system is unusable
alert // action must be taken immediately
crit // critical conditions
err // error conditions
warning // warning conditions
notice // normal but significant condition
info // informational
debug // debug-level messages
syslogLevelEmerg syslogLevel = iota // system is unusable
syslogLevelAlert // action must be taken immediately
syslogLevelCrit // critical conditions
syslogLevelErr // error conditions
syslogLevelWarn // warning conditions
syslogLevelNotice // normal but significant condition
syslogLevelInfo // informational
syslogLevelDebug // debug-level messages
)
func (l syslogLevel) String() string {
switch l {
case emerg:
case syslogLevelEmerg:
return "emerg"
case alert:
case syslogLevelAlert:
return "alert"
case crit:
case syslogLevelCrit:
return "crit"
case err:
case syslogLevelErr:
return "err"
case warning:
case syslogLevelWarn:
return "warning"
case notice:
case syslogLevelNotice:
return "notice"
case debug:
case syslogLevelDebug:
return "debug"
default:
return "info"
@@ -137,10 +153,10 @@ func Config() PHPConfig {
}
}
func calculateMaxThreads(opt *opt) (int, int, int, error) {
func calculateMaxThreads(opt *opt) (numWorkers int, _ error) {
maxProcs := runtime.GOMAXPROCS(0) * 2
maxThreadsFromWorkers := 0
var numWorkers int
for i, w := range opt.workers {
if w.num <= 0 {
// https://github.com/php/frankenphp/issues/126
@@ -149,33 +165,53 @@ func calculateMaxThreads(opt *opt) (int, int, int, error) {
metrics.TotalWorkers(w.name, w.num)
numWorkers += opt.workers[i].num
if w.maxThreads > 0 {
if w.maxThreads < w.num {
return 0, fmt.Errorf("worker max_threads (%d) must be greater or equal to worker num (%d) (%q)", w.maxThreads, w.num, w.fileName)
}
if w.maxThreads > opt.maxThreads && opt.maxThreads > 0 {
return 0, fmt.Errorf("worker max_threads (%d) cannot be greater than total max_threads (%d) (%q)", w.maxThreads, opt.maxThreads, w.fileName)
}
maxThreadsFromWorkers += w.maxThreads - w.num
}
}
numThreadsIsSet := opt.numThreads > 0
maxThreadsIsSet := opt.maxThreads != 0
maxThreadsIsAuto := opt.maxThreads < 0 // maxthreads < 0 signifies auto mode (see phpmaintread.go)
// if max_threads is only defined in workers, scale up to the sum of all worker max_threads
if !maxThreadsIsSet && maxThreadsFromWorkers > 0 {
maxThreadsIsSet = true
if numThreadsIsSet {
opt.maxThreads = opt.numThreads + maxThreadsFromWorkers
} else {
opt.maxThreads = numWorkers + 1 + maxThreadsFromWorkers
}
}
if numThreadsIsSet && !maxThreadsIsSet {
opt.maxThreads = opt.numThreads
if opt.numThreads <= numWorkers {
err := fmt.Errorf("num_threads (%d) must be greater than the number of worker threads (%d)", opt.numThreads, numWorkers)
return 0, 0, 0, err
return 0, fmt.Errorf("num_threads (%d) must be greater than the number of worker threads (%d)", opt.numThreads, numWorkers)
}
return opt.numThreads, numWorkers, opt.maxThreads, nil
return numWorkers, nil
}
if maxThreadsIsSet && !numThreadsIsSet {
opt.numThreads = numWorkers + 1
if !maxThreadsIsAuto && opt.numThreads > opt.maxThreads {
err := fmt.Errorf("max_threads (%d) must be greater than the number of worker threads (%d)", opt.maxThreads, numWorkers)
return 0, 0, 0, err
return 0, fmt.Errorf("max_threads (%d) must be greater than the number of worker threads (%d)", opt.maxThreads, numWorkers)
}
return opt.numThreads, numWorkers, opt.maxThreads, nil
return numWorkers, nil
}
if !numThreadsIsSet {
if !maxThreadsIsSet && !numThreadsIsSet {
if numWorkers >= maxProcs {
// Start at least as many threads as workers, and keep a free thread to handle requests in non-worker mode
opt.numThreads = numWorkers + 1
@@ -184,21 +220,19 @@ func calculateMaxThreads(opt *opt) (int, int, int, error) {
}
opt.maxThreads = opt.numThreads
return opt.numThreads, numWorkers, opt.maxThreads, nil
return numWorkers, nil
}
// both num_threads and max_threads are set
if opt.numThreads <= numWorkers {
err := fmt.Errorf("num_threads (%d) must be greater than the number of worker threads (%d)", opt.numThreads, numWorkers)
return 0, 0, 0, err
return 0, fmt.Errorf("num_threads (%d) must be greater than the number of worker threads (%d)", opt.numThreads, numWorkers)
}
if !maxThreadsIsAuto && opt.maxThreads < opt.numThreads {
err := fmt.Errorf("max_threads (%d) must be greater than or equal to num_threads (%d)", opt.maxThreads, opt.numThreads)
return 0, 0, 0, err
return 0, fmt.Errorf("max_threads (%d) must be greater than or equal to num_threads (%d)", opt.maxThreads, opt.numThreads)
}
return opt.numThreads, numWorkers, opt.maxThreads, nil
return numWorkers, nil
}
// Init starts the PHP runtime and the configured workers.
@@ -214,81 +248,105 @@ func Init(options ...Option) error {
registerExtensions()
// add registered external workers
for _, ew := range extensionWorkers {
options = append(options, WithWorkers(ew.Name(), ew.FileName(), ew.GetMinThreads(), WithWorkerEnv(ew.Env())))
}
opt := &opt{}
for _, o := range options {
if err := o(opt); err != nil {
Shutdown()
return err
}
}
if opt.logger == nil {
// set a default logger
// to disable logging, set the logger to slog.New(slog.NewTextHandler(io.Discard, nil))
l := slog.New(slog.NewTextHandler(os.Stdout, nil))
globalMu.Lock()
loggerMu.Lock()
logger = l
loggerMu.Unlock()
} else {
loggerMu.Lock()
logger = opt.logger
loggerMu.Unlock()
if opt.ctx != nil {
globalCtx = opt.ctx
opt.ctx = nil
}
if opt.logger != nil {
globalLogger = opt.logger
opt.logger = nil
}
globalMu.Unlock()
if opt.metrics != nil {
metrics = opt.metrics
}
maxWaitTime = opt.maxWaitTime
totalThreadCount, workerThreadCount, maxThreadCount, err := calculateMaxThreads(opt)
workerThreadCount, err := calculateMaxThreads(opt)
if err != nil {
Shutdown()
return err
}
metrics.TotalThreads(totalThreadCount)
metrics.TotalThreads(opt.numThreads)
config := Config()
if config.Version.MajorVersion < 8 || (config.Version.MajorVersion == 8 && config.Version.MinorVersion < 2) {
Shutdown()
return ErrInvalidPHPVersion
}
if config.ZTS {
if !config.ZendMaxExecutionTimers && runtime.GOOS == "linux" {
logger.Warn(`Zend Max Execution Timers are not enabled, timeouts (e.g. "max_execution_time") are disabled, recompile PHP with the "--enable-zend-max-execution-timers" configuration option to fix this issue`)
if globalLogger.Enabled(globalCtx, slog.LevelWarn) {
globalLogger.LogAttrs(globalCtx, slog.LevelWarn, `Zend Max Execution Timers are not enabled, timeouts (e.g. "max_execution_time") are disabled, recompile PHP with the "--enable-zend-max-execution-timers" configuration option to fix this issue`)
}
}
} else {
totalThreadCount = 1
logger.Warn(`ZTS is not enabled, only 1 thread will be available, recompile PHP using the "--enable-zts" configuration option or performance will be degraded`)
opt.numThreads = 1
if globalLogger.Enabled(globalCtx, slog.LevelWarn) {
globalLogger.LogAttrs(globalCtx, slog.LevelWarn, `ZTS is not enabled, only 1 thread will be available, recompile PHP using the "--enable-zts" configuration option or performance will be degraded`)
}
}
mainThread, err := initPHPThreads(totalThreadCount, maxThreadCount, opt.phpIni)
mainThread, err := initPHPThreads(opt.numThreads, opt.maxThreads, opt.phpIni)
if err != nil {
Shutdown()
return err
}
regularRequestChan = make(chan *frankenPHPContext, totalThreadCount-workerThreadCount)
regularThreads = make([]*phpThread, 0, totalThreadCount-workerThreadCount)
for i := 0; i < totalThreadCount-workerThreadCount; i++ {
regularRequestChan = make(chan contextHolder)
regularThreads = make([]*phpThread, 0, opt.numThreads-workerThreadCount)
for i := 0; i < opt.numThreads-workerThreadCount; i++ {
convertToRegularThread(getInactivePHPThread())
}
if err := initWorkers(opt.workers); err != nil {
Shutdown()
return err
}
if err := initWatchers(opt); err != nil {
Shutdown()
return err
}
initAutoScaling(mainThread)
ctx := context.Background()
logger.LogAttrs(ctx, slog.LevelInfo, "FrankenPHP started 🐘", slog.String("php_version", Version().Version), slog.Int("num_threads", mainThread.numThreads), slog.Int("max_threads", mainThread.maxThreads))
if EmbeddedAppPath != "" {
logger.LogAttrs(ctx, slog.LevelInfo, "embedded PHP app 📦", slog.String("path", EmbeddedAppPath))
if globalLogger.Enabled(globalCtx, slog.LevelInfo) {
globalLogger.LogAttrs(globalCtx, slog.LevelInfo, "FrankenPHP started 🐘", slog.String("php_version", Version().Version), slog.Int("num_threads", mainThread.numThreads), slog.Int("max_threads", mainThread.maxThreads))
if EmbeddedAppPath != "" {
globalLogger.LogAttrs(globalCtx, slog.LevelInfo, "embedded PHP app 📦", slog.String("path", EmbeddedAppPath))
}
}
// register the startup/shutdown hooks (mainly useful for extensions)
onServerShutdown = nil
for _, w := range opt.workers {
if w.onServerStartup != nil {
w.onServerStartup()
}
if w.onServerShutdown != nil {
onServerShutdown = append(onServerShutdown, w.onServerShutdown)
}
}
return nil
@@ -300,7 +358,12 @@ func Shutdown() {
return
}
drainWatcher()
// call the shutdown hooks (mainly useful for extensions)
for _, fn := range onServerShutdown {
fn()
}
drainWatchers()
drainAutoScaling()
drainPHPThreads()
@@ -312,41 +375,52 @@ func Shutdown() {
}
isRunning = false
logger.Debug("FrankenPHP shut down")
if globalLogger.Enabled(globalCtx, slog.LevelDebug) {
globalLogger.LogAttrs(globalCtx, slog.LevelDebug, "FrankenPHP shut down")
}
resetGlobals()
}
// ServeHTTP executes a PHP script according to the given context.
func ServeHTTP(responseWriter http.ResponseWriter, request *http.Request) error {
h := responseWriter.Header()
if h["Server"] == nil {
h["Server"] = serverHeader
}
if !isRunning {
return ErrNotRunning
}
fc, ok := fromContext(request.Context())
ctx := request.Context()
fc, ok := fromContext(ctx)
ch := contextHolder{ctx, fc}
if !ok {
return ErrInvalidRequest
}
fc.responseWriter = responseWriter
if !fc.validate() {
return nil
if err := fc.validate(); err != nil {
return err
}
// Detect if a worker is available to handle this request
if fc.worker != nil {
fc.worker.handleRequest(fc)
return nil
return fc.worker.handleRequest(ch)
}
// If no worker was available, send the request to non-worker threads
handleRequestWithRegularPHPThreads(fc)
return nil
return handleRequestWithRegularPHPThreads(ch)
}
//export go_ub_write
func go_ub_write(threadIndex C.uintptr_t, cBuf *C.char, length C.int) (C.size_t, C.bool) {
fc := phpThreads[threadIndex].getRequestContext()
thread := phpThreads[threadIndex]
fc := thread.frankenPHPContext()
if fc.isDone {
return 0, C.bool(true)
@@ -361,14 +435,27 @@ func go_ub_write(threadIndex C.uintptr_t, cBuf *C.char, length C.int) (C.size_t,
writer = fc.responseWriter
}
var ctx context.Context
i, e := writer.Write(unsafe.Slice((*byte)(unsafe.Pointer(cBuf)), length))
if e != nil {
fc.logger.LogAttrs(context.Background(), slog.LevelWarn, "write error", slog.Any("error", e))
ctx = thread.context()
if fc.logger.Enabled(ctx, slog.LevelWarn) {
fc.logger.LogAttrs(ctx, slog.LevelWarn, "write error", slog.Any("error", e))
}
}
if fc.responseWriter == nil {
// probably starting a worker script, log the output
fc.logger.Info(writer.(*bytes.Buffer).String())
if ctx == nil {
ctx = thread.context()
}
if fc.logger.Enabled(ctx, slog.LevelInfo) {
fc.logger.LogAttrs(ctx, slog.LevelInfo, writer.(*bytes.Buffer).String())
}
}
return C.size_t(i), C.bool(fc.clientHasClosed())
@@ -377,12 +464,15 @@ func go_ub_write(threadIndex C.uintptr_t, cBuf *C.char, length C.int) (C.size_t,
//export go_apache_request_headers
func go_apache_request_headers(threadIndex C.uintptr_t) (*C.go_string, C.size_t) {
thread := phpThreads[threadIndex]
fc := thread.getRequestContext()
ctx := thread.context()
fc := thread.frankenPHPContext()
if fc.responseWriter == nil {
// worker mode, not handling a request
logger.LogAttrs(context.Background(), slog.LevelDebug, "apache_request_headers() called in non-HTTP context", slog.String("worker", fc.worker.name))
if globalLogger.Enabled(ctx, slog.LevelDebug) {
globalLogger.LogAttrs(ctx, slog.LevelDebug, "apache_request_headers() called in non-HTTP context", slog.String("worker", fc.worker.name))
}
return nil, 0
}
@@ -410,10 +500,13 @@ func go_apache_request_headers(threadIndex C.uintptr_t) (*C.go_string, C.size_t)
return sd, C.size_t(len(fc.request.Header))
}
func addHeader(fc *frankenPHPContext, cString *C.char, length C.int) {
func addHeader(ctx context.Context, fc *frankenPHPContext, cString *C.char, length C.int) {
key, val := splitRawHeader(cString, int(length))
if key == "" {
fc.logger.LogAttrs(context.Background(), slog.LevelDebug, "invalid header", slog.String("header", C.GoStringN(cString, length)))
if fc.logger.Enabled(ctx, slog.LevelDebug) {
fc.logger.LogAttrs(ctx, slog.LevelDebug, "invalid header", slog.String("header", C.GoStringN(cString, length)))
}
return
}
fc.responseWriter.Header().Add(key, val)
@@ -452,8 +545,8 @@ func splitRawHeader(rawHeader *C.char, length int) (string, string) {
//export go_write_headers
func go_write_headers(threadIndex C.uintptr_t, status C.int, headers *C.zend_llist) C.bool {
fc := phpThreads[threadIndex].getRequestContext()
thread := phpThreads[threadIndex]
fc := thread.frankenPHPContext()
if fc == nil {
return C.bool(false)
}
@@ -471,7 +564,7 @@ func go_write_headers(threadIndex C.uintptr_t, status C.int, headers *C.zend_lli
for current != nil {
h := (*C.sapi_header_struct)(unsafe.Pointer(&(current.data)))
addHeader(fc, h.header, C.int(h.header_len))
addHeader(thread.context(), fc, h.header, C.int(h.header_len))
current = current.next
}
@@ -480,13 +573,18 @@ func go_write_headers(threadIndex C.uintptr_t, status C.int, headers *C.zend_lli
// go panics on invalid status code
// https://github.com/golang/go/blob/9b8742f2e79438b9442afa4c0a0139d3937ea33f/src/net/http/server.go#L1162
if goStatus < 100 || goStatus > 999 {
logger.Warn(fmt.Sprintf("Invalid response status code %v", goStatus))
ctx := thread.context()
if globalLogger.Enabled(ctx, slog.LevelWarn) {
globalLogger.LogAttrs(ctx, slog.LevelWarn, "Invalid response status code", slog.Int("status_code", goStatus))
}
goStatus = 500
}
fc.responseWriter.WriteHeader(goStatus)
if goStatus >= 100 && goStatus < 200 {
if goStatus < 200 {
// Clear headers, it's not automatically done by ResponseWriter.WriteHeader() for 1xx responses
h := fc.responseWriter.Header()
for k := range h {
@@ -499,8 +597,13 @@ func go_write_headers(threadIndex C.uintptr_t, status C.int, headers *C.zend_lli
//export go_sapi_flush
func go_sapi_flush(threadIndex C.uintptr_t) bool {
fc := phpThreads[threadIndex].getRequestContext()
if fc == nil || fc.responseWriter == nil {
thread := phpThreads[threadIndex]
fc := thread.frankenPHPContext()
if fc == nil {
return false
}
if fc.responseWriter == nil {
return false
}
@@ -509,7 +612,11 @@ func go_sapi_flush(threadIndex C.uintptr_t) bool {
}
if err := http.NewResponseController(fc.responseWriter).Flush(); err != nil {
logger.LogAttrs(context.Background(), slog.LevelWarn, "the current responseWriter is not a flusher, if you are not using a custom build, please report this issue", slog.Any("error", err))
ctx := thread.context()
if globalLogger.Enabled(ctx, slog.LevelWarn) {
globalLogger.LogAttrs(ctx, slog.LevelWarn, "the current responseWriter is not a flusher, if you are not using a custom build, please report this issue", slog.Any("error", err))
}
}
return false
@@ -517,7 +624,7 @@ func go_sapi_flush(threadIndex C.uintptr_t) bool {
//export go_read_post
func go_read_post(threadIndex C.uintptr_t, cBuf *C.char, countBytes C.size_t) (readBytes C.size_t) {
fc := phpThreads[threadIndex].getRequestContext()
fc := phpThreads[threadIndex].frankenPHPContext()
if fc.responseWriter == nil {
return 0
@@ -536,7 +643,7 @@ func go_read_post(threadIndex C.uintptr_t, cBuf *C.char, countBytes C.size_t) (r
//export go_read_cookies
func go_read_cookies(threadIndex C.uintptr_t) *C.char {
request := phpThreads[threadIndex].getRequestContext().request
request := phpThreads[threadIndex].frankenPHPContext().request
if request == nil {
return nil
}
@@ -553,34 +660,97 @@ func go_read_cookies(threadIndex C.uintptr_t) *C.char {
return C.CString(cookie)
}
//export go_log
func go_log(message *C.char, level C.int) {
m := C.GoString(message)
func getLogger(threadIndex C.uintptr_t) (*slog.Logger, context.Context) {
ctxHolder := phpThreads[threadIndex]
if ctxHolder == nil {
return globalLogger, globalCtx
}
var le syslogLevel
if level < C.int(emerg) || level > C.int(debug) {
le = info
} else {
ctx := ctxHolder.context()
if ctxHolder.handler == nil {
return globalLogger, ctx
}
fCtx := ctxHolder.frankenPHPContext()
if fCtx == nil || fCtx.logger == nil {
return globalLogger, ctx
}
return fCtx.logger, ctx
}
//export go_log
func go_log(threadIndex C.uintptr_t, message *C.char, level C.int) {
logger, ctx := getLogger(threadIndex)
m := C.GoString(message)
le := syslogLevelInfo
if level >= C.int(syslogLevelEmerg) && level <= C.int(syslogLevelDebug) {
le = syslogLevel(level)
}
switch le {
case emerg, alert, crit, err:
logger.LogAttrs(context.Background(), slog.LevelError, m, slog.String("syslog_level", syslogLevel(level).String()))
case syslogLevelEmerg, syslogLevelAlert, syslogLevelCrit, syslogLevelErr:
if logger.Enabled(ctx, slog.LevelError) {
logger.LogAttrs(ctx, slog.LevelError, m, slog.String("syslog_level", le.String()))
}
case warning:
logger.LogAttrs(context.Background(), slog.LevelWarn, m, slog.String("syslog_level", syslogLevel(level).String()))
case debug:
logger.LogAttrs(context.Background(), slog.LevelDebug, m, slog.String("syslog_level", syslogLevel(level).String()))
case syslogLevelWarn:
if logger.Enabled(ctx, slog.LevelWarn) {
logger.LogAttrs(ctx, slog.LevelWarn, m, slog.String("syslog_level", le.String()))
}
case syslogLevelDebug:
if logger.Enabled(ctx, slog.LevelDebug) {
logger.LogAttrs(ctx, slog.LevelDebug, m, slog.String("syslog_level", le.String()))
}
default:
logger.LogAttrs(context.Background(), slog.LevelInfo, m, slog.String("syslog_level", syslogLevel(level).String()))
if logger.Enabled(ctx, slog.LevelInfo) {
logger.LogAttrs(ctx, slog.LevelInfo, m, slog.String("syslog_level", le.String()))
}
}
}
//export go_log_attrs
func go_log_attrs(threadIndex C.uintptr_t, message *C.zend_string, cLevel C.zend_long, cAttrs *C.zval) *C.char {
logger, ctx := getLogger(threadIndex)
level := slog.Level(cLevel)
if !logger.Enabled(ctx, level) {
return nil
}
var attrs map[string]any
if cAttrs != nil {
var err error
if attrs, err = GoMap[any](unsafe.Pointer(*(**C.zend_array)(unsafe.Pointer(&cAttrs.value[0])))); err != nil {
// PHP exception message.
return C.CString("Failed to log message: converting attrs: " + err.Error())
}
}
logger.LogAttrs(ctx, level, GoString(unsafe.Pointer(message)), mapToAttr(attrs)...)
return nil
}
func mapToAttr(input map[string]any) []slog.Attr {
out := make([]slog.Attr, 0, len(input))
for key, val := range input {
out = append(out, slog.Any(key, val))
}
return out
}
//export go_is_context_done
func go_is_context_done(threadIndex C.uintptr_t) C.bool {
return C.bool(phpThreads[threadIndex].getRequestContext().isDone)
return C.bool(phpThreads[threadIndex].frankenPHPContext().isDone)
}
// ExecuteScriptCLI executes the PHP script passed as parameter.
@@ -629,3 +799,12 @@ func timeoutChan(timeout time.Duration) <-chan time.Time {
return time.After(timeout)
}
func resetGlobals() {
globalMu.Lock()
globalCtx = context.Background()
globalLogger = slog.Default()
workers = nil
watcherIsEnabled = false
globalMu.Unlock()
}

View File

@@ -23,12 +23,6 @@ typedef struct ht_key_value_pair {
size_t val_len;
} ht_key_value_pair;
typedef struct php_variable {
const char *var;
size_t data_len;
char *data;
} php_variable;
typedef struct frankenphp_version {
unsigned char major_version;
unsigned char minor_version;
@@ -40,7 +34,6 @@ typedef struct frankenphp_version {
frankenphp_version frankenphp_get_version();
typedef struct frankenphp_config {
frankenphp_version version;
bool zts;
bool zend_signals;
bool zend_max_execution_timers;
@@ -52,6 +45,7 @@ bool frankenphp_new_php_thread(uintptr_t thread_index);
bool frankenphp_shutdown_dummy_request(void);
int frankenphp_execute_script(char *file_name);
void frankenphp_update_local_thread_context(bool is_worker);
int frankenphp_execute_script_cli(char *script, int argc, char **argv,
bool eval);
@@ -65,8 +59,6 @@ void frankenphp_register_variable_safe(char *key, char *var, size_t val_len,
zend_string *frankenphp_init_persistent_string(const char *string, size_t len);
int frankenphp_reset_opcache(void);
int frankenphp_get_current_memory_limit();
void frankenphp_add_assoc_str_ex(zval *track_vars_array, char *key,
size_t keylen, zend_string *val);
void frankenphp_register_single(zend_string *z_key, char *value, size_t val_len,
zval *track_vars_array);

View File

@@ -2,6 +2,18 @@
/** @generate-class-entries */
/** @var int */
const FRANKENPHP_LOG_LEVEL_DEBUG = -4;
/** @var int */
const FRANKENPHP_LOG_LEVEL_INFO = 0;
/** @var int */
const FRANKENPHP_LOG_LEVEL_WARN = 4;
/** @var int */
const FRANKENPHP_LOG_LEVEL_ERROR = 8;
function frankenphp_handle_request(callable $callback): bool {}
function headers_send(int $status = 200): int {}
@@ -32,3 +44,13 @@ function frankenphp_response_headers(): array|bool {}
*/
function apache_response_headers(): array|bool {}
/**
* @param string|string[] $topics
*/
function mercure_publish(string|array $topics, string $data = '', bool $private = false, ?string $id = null, ?string $type = null, ?int $retry = null): string {}
/**
* @param int $level The importance or severity of a log event. The higher the level, the more important or severe the event. For more details, see: https://pkg.go.dev/log/slog#Level
* array<string, any> $context Values of the array will be converted to the corresponding Go type (if supported by FrankenPHP) and added to the context of the structured logs using https://pkg.go.dev/log/slog#Attr
*/
function frankenphp_log(string $message, int $level = 0, array $context = []): void {}

View File

@@ -1,52 +1,75 @@
/* This is a generated file, edit the .stub.php file instead.
* Stub hash: 05ebde17137c559e891362fba6524fad1e0a2dfe */
* Stub hash: 60f0d27c04f94d7b24c052e91ef294595a2bc421 */
ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_frankenphp_handle_request, 0, 1,
_IS_BOOL, 0)
ZEND_ARG_TYPE_INFO(0, callback, IS_CALLABLE, 0)
ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_frankenphp_handle_request, 0, 1, _IS_BOOL, 0)
ZEND_ARG_TYPE_INFO(0, callback, IS_CALLABLE, 0)
ZEND_END_ARG_INFO()
ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_headers_send, 0, 0, IS_LONG, 0)
ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, status, IS_LONG, 0, "200")
ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, status, IS_LONG, 0, "200")
ZEND_END_ARG_INFO()
ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_frankenphp_finish_request, 0, 0,
_IS_BOOL, 0)
ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_frankenphp_finish_request, 0, 0, _IS_BOOL, 0)
ZEND_END_ARG_INFO()
#define arginfo_fastcgi_finish_request arginfo_frankenphp_finish_request
ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_frankenphp_request_headers, 0,
0, IS_ARRAY, 0)
ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_frankenphp_request_headers, 0, 0, IS_ARRAY, 0)
ZEND_END_ARG_INFO()
#define arginfo_apache_request_headers arginfo_frankenphp_request_headers
#define arginfo_getallheaders arginfo_frankenphp_request_headers
ZEND_BEGIN_ARG_WITH_RETURN_TYPE_MASK_EX(arginfo_frankenphp_response_headers, 0,
0, MAY_BE_ARRAY | MAY_BE_BOOL)
ZEND_BEGIN_ARG_WITH_RETURN_TYPE_MASK_EX(arginfo_frankenphp_response_headers, 0, 0, MAY_BE_ARRAY|MAY_BE_BOOL)
ZEND_END_ARG_INFO()
#define arginfo_apache_response_headers arginfo_frankenphp_response_headers
ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_mercure_publish, 0, 1, IS_STRING, 0)
ZEND_ARG_TYPE_MASK(0, topics, MAY_BE_STRING|MAY_BE_ARRAY, NULL)
ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, data, IS_STRING, 0, "\'\'")
ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, private, _IS_BOOL, 0, "false")
ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, id, IS_STRING, 1, "null")
ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, type, IS_STRING, 1, "null")
ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, retry, IS_LONG, 1, "null")
ZEND_END_ARG_INFO()
ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_frankenphp_log, 0, 1, IS_VOID, 0)
ZEND_ARG_TYPE_INFO(0, message, IS_STRING, 0)
ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, level, IS_LONG, 0, "0")
ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, context, IS_ARRAY, 0, "[]")
ZEND_END_ARG_INFO()
ZEND_FUNCTION(frankenphp_handle_request);
ZEND_FUNCTION(headers_send);
ZEND_FUNCTION(frankenphp_finish_request);
ZEND_FUNCTION(frankenphp_request_headers);
ZEND_FUNCTION(frankenphp_response_headers);
ZEND_FUNCTION(mercure_publish);
ZEND_FUNCTION(frankenphp_log);
// clang-format off
static const zend_function_entry ext_functions[] = {
ZEND_FE(frankenphp_handle_request, arginfo_frankenphp_handle_request)
ZEND_FE(headers_send, arginfo_headers_send)
ZEND_FE(frankenphp_finish_request, arginfo_frankenphp_finish_request)
ZEND_FALIAS(fastcgi_finish_request, frankenphp_finish_request, arginfo_fastcgi_finish_request)
ZEND_FE(frankenphp_request_headers, arginfo_frankenphp_request_headers)
ZEND_FALIAS(apache_request_headers, frankenphp_request_headers, arginfo_apache_request_headers)
ZEND_FALIAS(getallheaders, frankenphp_request_headers, arginfo_getallheaders)
ZEND_FE(frankenphp_response_headers, arginfo_frankenphp_response_headers)
ZEND_FALIAS(apache_response_headers, frankenphp_response_headers, arginfo_apache_response_headers)
ZEND_FE_END
ZEND_FE(frankenphp_handle_request, arginfo_frankenphp_handle_request)
ZEND_FE(headers_send, arginfo_headers_send)
ZEND_FE(frankenphp_finish_request, arginfo_frankenphp_finish_request)
ZEND_FALIAS(fastcgi_finish_request, frankenphp_finish_request, arginfo_fastcgi_finish_request)
ZEND_FE(frankenphp_request_headers, arginfo_frankenphp_request_headers)
ZEND_FALIAS(apache_request_headers, frankenphp_request_headers, arginfo_apache_request_headers)
ZEND_FALIAS(getallheaders, frankenphp_request_headers, arginfo_getallheaders)
ZEND_FE(frankenphp_response_headers, arginfo_frankenphp_response_headers)
ZEND_FALIAS(apache_response_headers, frankenphp_response_headers, arginfo_apache_response_headers)
ZEND_FE(mercure_publish, arginfo_mercure_publish)
ZEND_FE(frankenphp_log, arginfo_frankenphp_log)
ZEND_FE_END
};
// clang-format on
static void register_frankenphp_symbols(int module_number)
{
REGISTER_LONG_CONSTANT("FRANKENPHP_LOG_LEVEL_DEBUG", -4, CONST_PERSISTENT);
REGISTER_LONG_CONSTANT("FRANKENPHP_LOG_LEVEL_INFO", 0, CONST_PERSISTENT);
REGISTER_LONG_CONSTANT("FRANKENPHP_LOG_LEVEL_WARN", 4, CONST_PERSISTENT);
REGISTER_LONG_CONSTANT("FRANKENPHP_LOG_LEVEL_ERROR", 8, CONST_PERSISTENT);
}

View File

@@ -8,6 +8,7 @@ import (
"bytes"
"context"
"errors"
"flag"
"fmt"
"io"
"log"
@@ -31,10 +32,6 @@ import (
"github.com/dunglas/frankenphp/internal/fastabs"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uber.org/zap/exp/zapslog"
"go.uber.org/zap/zapcore"
"go.uber.org/zap/zaptest"
"go.uber.org/zap/zaptest/observer"
)
type testOptions struct {
@@ -46,6 +43,7 @@ type testOptions struct {
realServer bool
logger *slog.Logger
initOpts []frankenphp.Option
requestOpts []frankenphp.RequestOption
phpIni map[string]string
}
@@ -60,10 +58,6 @@ func runTest(t *testing.T, test func(func(http.ResponseWriter, *http.Request), *
cwd, _ := os.Getwd()
testDataDir := cwd + "/testdata/"
if opts.logger == nil {
opts.logger = slog.New(zapslog.NewHandler(zaptest.NewLogger(t).Core()))
}
initOpts := []frankenphp.Option{frankenphp.WithLogger(opts.logger)}
if opts.workerScript != "" {
workerOpts := []frankenphp.WorkerOption{
@@ -78,15 +72,19 @@ func runTest(t *testing.T, test func(func(http.ResponseWriter, *http.Request), *
}
err := frankenphp.Init(initOpts...)
require.Nil(t, err)
require.NoError(t, err)
defer frankenphp.Shutdown()
opts.requestOpts = append(opts.requestOpts, frankenphp.WithRequestDocumentRoot(testDataDir, false))
handler := func(w http.ResponseWriter, r *http.Request) {
req, err := frankenphp.NewRequestWithContext(r, frankenphp.WithRequestDocumentRoot(testDataDir, false))
req, err := frankenphp.NewRequestWithContext(r, opts.requestOpts...)
assert.NoError(t, err)
err = frankenphp.ServeHTTP(w, req)
assert.NoError(t, err)
if err != nil && !errors.As(err, &frankenphp.ErrRejected{}) {
assert.Fail(t, fmt.Sprintf("Received unexpected error:\n%+v", err))
}
}
var ts *httptest.Server
@@ -109,6 +107,7 @@ func runTest(t *testing.T, test func(func(http.ResponseWriter, *http.Request), *
func testRequest(req *http.Request, handler func(http.ResponseWriter, *http.Request), t *testing.T) (string, *http.Response) {
t.Helper()
w := httptest.NewRecorder()
handler(w, req)
resp := w.Result()
@@ -133,6 +132,16 @@ func testPost(url string, body string, handler func(http.ResponseWriter, *http.R
return testRequest(req, handler, t)
}
func TestMain(m *testing.M) {
flag.Parse()
if !testing.Verbose() {
slog.SetDefault(slog.New(slog.DiscardHandler))
}
os.Exit(m.Run())
}
func TestHelloWorld_module(t *testing.T) { testHelloWorld(t, nil) }
func TestHelloWorld_worker(t *testing.T) {
testHelloWorld(t, &testOptions{workerScript: "index.php"})
@@ -406,36 +415,61 @@ my_autoloader`, i), body)
}, opts)
}
func TestLog_module(t *testing.T) { testLog(t, &testOptions{}) }
func TestLog_worker(t *testing.T) {
testLog(t, &testOptions{workerScript: "log.php"})
func TestLog_error_log_module(t *testing.T) { testLog_error_log(t, &testOptions{}) }
func TestLog_error_log_worker(t *testing.T) {
testLog_error_log(t, &testOptions{workerScript: "log-error_log.php"})
}
func testLog(t *testing.T, opts *testOptions) {
logger, logs := observer.New(zapcore.InfoLevel)
opts.logger = slog.New(zapslog.NewHandler(logger))
func testLog_error_log(t *testing.T, opts *testOptions) {
var buf fmt.Stringer
opts.logger, buf = newTestLogger(t)
runTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, i int) {
req := httptest.NewRequest("GET", fmt.Sprintf("http://example.com/log.php?i=%d", i), nil)
req := httptest.NewRequest("GET", fmt.Sprintf("http://example.com/log-error_log.php?i=%d", i), nil)
w := httptest.NewRecorder()
handler(w, req)
for logs.FilterMessage(fmt.Sprintf("request %d", i)).Len() <= 0 {
assert.Contains(t, buf.String(), fmt.Sprintf("request %d", i))
}, opts)
}
func TestLog_frankenphp_log_module(t *testing.T) { testLog_frankenphp_log(t, &testOptions{}) }
func TestLog_frankenphp_log_worker(t *testing.T) {
testLog_frankenphp_log(t, &testOptions{workerScript: "log-frankenphp_log.php"})
}
func testLog_frankenphp_log(t *testing.T, opts *testOptions) {
var buf fmt.Stringer
opts.logger, buf = newTestLogger(t)
runTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, i int) {
req := httptest.NewRequest("GET", fmt.Sprintf("http://example.com/log-frankenphp_log.php?i=%d", i), nil)
w := httptest.NewRecorder()
handler(w, req)
logs := buf.String()
for _, message := range []string{
`level=INFO msg="default level message"`,
fmt.Sprintf(`level=DEBUG msg="some debug message %d" "key int"=1`, i),
fmt.Sprintf(`level=INFO msg="some info message %d" "key string"=string`, i),
fmt.Sprintf(`level=WARN msg="some warn message %d"`, i),
fmt.Sprintf(`level=ERROR msg="some error message %d" err="[a v]"`, i),
} {
assert.Contains(t, logs, message)
}
}, opts)
}
func TestConnectionAbort_module(t *testing.T) { testConnectionAbort(t, &testOptions{}) }
func TestConnectionAbort_worker(t *testing.T) {
testConnectionAbort(t, &testOptions{workerScript: "connectionStatusLog.php"})
testConnectionAbort(t, &testOptions{workerScript: "connection_status.php"})
}
func testConnectionAbort(t *testing.T, opts *testOptions) {
testFinish := func(finish string) {
t.Run(fmt.Sprintf("finish=%s", finish), func(t *testing.T) {
logger, logs := observer.New(zapcore.InfoLevel)
opts.logger = slog.New(zapslog.NewHandler(logger))
var buf syncBuffer
opts.logger = slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{Level: slog.LevelInfo}))
runTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, i int) {
req := httptest.NewRequest("GET", fmt.Sprintf("http://example.com/connectionStatusLog.php?i=%d&finish=%s", i, finish), nil)
req := httptest.NewRequest("GET", fmt.Sprintf("http://example.com/connection_status.php?i=%d&finish=%s", i, finish), nil)
w := httptest.NewRecorder()
ctx, cancel := context.WithCancel(req.Context())
@@ -443,7 +477,7 @@ func testConnectionAbort(t *testing.T, opts *testOptions) {
cancel()
handler(w, req)
for logs.FilterMessage(fmt.Sprintf("request %d: 1", i)).Len() <= 0 {
for !strings.Contains(buf.String(), fmt.Sprintf("request %d: 1", i)) {
}
}, opts)
})
@@ -601,10 +635,13 @@ func testRequestHeaders(t *testing.T, opts *testOptions) {
}
func TestFailingWorker(t *testing.T) {
runTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, i int) {
body, _ := testGet("http://example.com/failing-worker.php", handler, t)
assert.Contains(t, body, "ok")
}, &testOptions{workerScript: "failing-worker.php"})
t.Cleanup(frankenphp.Shutdown)
err := frankenphp.Init(
frankenphp.WithWorkers("failing worker", "testdata/failing-worker.php", 4, frankenphp.WithWorkerMaxFailures(1)),
frankenphp.WithNumThreads(5),
)
assert.Error(t, err, "should return an immediate error if workers fail on startup")
}
func TestEnv(t *testing.T) {
@@ -759,22 +796,18 @@ func ExampleExecuteScriptCLI() {
}
func BenchmarkHelloWorld(b *testing.B) {
if err := frankenphp.Init(frankenphp.WithLogger(slog.New(slog.NewTextHandler(io.Discard, nil)))); err != nil {
panic(err)
}
defer frankenphp.Shutdown()
require.NoError(b, frankenphp.Init())
b.Cleanup(frankenphp.Shutdown)
cwd, _ := os.Getwd()
testDataDir := cwd + "/testdata/"
opt := frankenphp.WithRequestDocumentRoot(testDataDir, false)
handler := func(w http.ResponseWriter, r *http.Request) {
req, err := frankenphp.NewRequestWithContext(r, frankenphp.WithRequestDocumentRoot(testDataDir, false))
if err != nil {
panic(err)
}
req, err := frankenphp.NewRequestWithContext(r, opt)
require.NoError(b, err)
if err := frankenphp.ServeHTTP(w, req); err != nil {
panic(err)
}
require.NoError(b, frankenphp.ServeHTTP(w, req))
}
req := httptest.NewRequest("GET", "http://example.com/index.php", nil)
@@ -786,21 +819,18 @@ func BenchmarkHelloWorld(b *testing.B) {
}
func BenchmarkEcho(b *testing.B) {
if err := frankenphp.Init(frankenphp.WithLogger(slog.New(slog.NewTextHandler(io.Discard, nil)))); err != nil {
panic(err)
}
defer frankenphp.Shutdown()
require.NoError(b, frankenphp.Init())
b.Cleanup(frankenphp.Shutdown)
cwd, _ := os.Getwd()
testDataDir := cwd + "/testdata/"
opt := frankenphp.WithRequestDocumentRoot(testDataDir, false)
handler := func(w http.ResponseWriter, r *http.Request) {
req, err := frankenphp.NewRequestWithContext(r, frankenphp.WithRequestDocumentRoot(testDataDir, false))
if err != nil {
panic(err)
}
if err := frankenphp.ServeHTTP(w, req); err != nil {
panic(err)
}
req, err := frankenphp.NewRequestWithContext(r, opt)
require.NoError(b, err)
require.NoError(b, frankenphp.ServeHTTP(w, req))
}
const body = `{
@@ -852,10 +882,9 @@ func BenchmarkEcho(b *testing.B) {
}
func BenchmarkServerSuperGlobal(b *testing.B) {
if err := frankenphp.Init(frankenphp.WithLogger(slog.New(slog.NewTextHandler(io.Discard, nil)))); err != nil {
panic(err)
}
defer frankenphp.Shutdown()
require.NoError(b, frankenphp.Init())
b.Cleanup(frankenphp.Shutdown)
cwd, _ := os.Getwd()
testDataDir := cwd + "/testdata/"
@@ -900,16 +929,62 @@ func BenchmarkServerSuperGlobal(b *testing.B) {
preparedEnv := frankenphp.PrepareEnv(env)
opts := []frankenphp.RequestOption{frankenphp.WithRequestDocumentRoot(testDataDir, false), frankenphp.WithRequestPreparedEnv(preparedEnv)}
handler := func(w http.ResponseWriter, r *http.Request) {
req, err := frankenphp.NewRequestWithContext(r, frankenphp.WithRequestDocumentRoot(testDataDir, false), frankenphp.WithRequestPreparedEnv(preparedEnv))
if err != nil {
panic(err)
}
req, err := frankenphp.NewRequestWithContext(r, opts...)
require.NoError(b, err)
r.Header = headers
if err := frankenphp.ServeHTTP(w, req); err != nil {
panic(err)
}
require.NoError(b, frankenphp.ServeHTTP(w, req))
}
req := httptest.NewRequest("GET", "http://example.com/server-variable.php", nil)
w := httptest.NewRecorder()
for b.Loop() {
handler(w, req)
}
}
func BenchmarkUncommonHeaders(b *testing.B) {
require.NoError(b, frankenphp.Init())
b.Cleanup(frankenphp.Shutdown)
cwd, _ := os.Getwd()
testDataDir := cwd + "/testdata/"
// Mimics headers of a request sent by Firefox to GitHub
headers := http.Header{}
headers.Add(strings.Clone("Accept"), strings.Clone("text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8"))
headers.Add(strings.Clone("Accept-Encoding"), strings.Clone("gzip, deflate, br"))
headers.Add(strings.Clone("Accept-Language"), strings.Clone("fr,fr-FR;q=0.8,en-US;q=0.5,en;q=0.3"))
headers.Add(strings.Clone("Cache-Control"), strings.Clone("no-cache"))
headers.Add(strings.Clone("Connection"), strings.Clone("keep-alive"))
headers.Add(strings.Clone("Cookie"), strings.Clone("user_session=myrandomuuid; __Host-user_session_same_site=myotherrandomuuid; dotcom_user=dunglas; logged_in=yes; _foo=barbarbarbarbarbar; _device_id=anotherrandomuuid; color_mode=foobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobar; preferred_color_mode=light; tz=Europe%2FParis; has_recent_activity=1"))
headers.Add(strings.Clone("DNT"), strings.Clone("1"))
headers.Add(strings.Clone("Host"), strings.Clone("example.com"))
headers.Add(strings.Clone("Pragma"), strings.Clone("no-cache"))
headers.Add(strings.Clone("Sec-Fetch-Dest"), strings.Clone("document"))
headers.Add(strings.Clone("Sec-Fetch-Mode"), strings.Clone("navigate"))
headers.Add(strings.Clone("Sec-Fetch-Site"), strings.Clone("cross-site"))
headers.Add(strings.Clone("Sec-GPC"), strings.Clone("1"))
headers.Add(strings.Clone("Upgrade-Insecure-Requests"), strings.Clone("1"))
headers.Add(strings.Clone("User-Agent"), strings.Clone("Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:122.0) Gecko/20100101 Firefox/122.0"))
// Some uncommon headers
headers.Add(strings.Clone("X-Super-Custom"), strings.Clone("Foo"))
headers.Add(strings.Clone("Super-Super-Custom"), strings.Clone("Foo"))
headers.Add(strings.Clone("Super-Super-Custom"), strings.Clone("Bar"))
headers.Add(strings.Clone("Very-Custom"), strings.Clone("1"))
opt := frankenphp.WithRequestDocumentRoot(testDataDir, false)
handler := func(w http.ResponseWriter, r *http.Request) {
req, err := frankenphp.NewRequestWithContext(r, opt)
require.NoError(b, err)
r.Header = headers
require.NoError(b, frankenphp.ServeHTTP(w, req))
}
req := httptest.NewRequest("GET", "http://example.com/server-variable.php", nil)
@@ -942,7 +1017,7 @@ func testRejectInvalidHeaders(t *testing.T, opts *testOptions) {
}
func TestFlushEmptyResponse_module(t *testing.T) { testFlushEmptyResponse(t, &testOptions{}) }
func TestFlushEmptyRespnse_worker(t *testing.T) {
func TestFlushEmptyResponse_worker(t *testing.T) {
testFlushEmptyResponse(t, &testOptions{workerScript: "only-headers.php"})
}
@@ -988,7 +1063,8 @@ func FuzzRequest(f *testing.F) {
// The response status must be 400 if the request path contains null bytes
if strings.Contains(req.URL.Path, "\x00") {
assert.Equal(t, 400, resp.StatusCode)
assert.Contains(t, body, "Invalid request path")
assert.Contains(t, body, "invalid request path")
return
}
@@ -999,7 +1075,6 @@ func FuzzRequest(f *testing.F) {
// Headers should always be present even if empty
assert.Contains(t, body, fmt.Sprintf("[CONTENT_TYPE] => %s", fuzzedString))
assert.Contains(t, body, fmt.Sprintf("[HTTP_FUZZED] => %s", fuzzedString))
}, &testOptions{workerScript: "request-headers.php"})
})
}

52
go.mod
View File

@@ -1,46 +1,66 @@
module github.com/dunglas/frankenphp
go 1.25.0
go 1.25.4
retract v1.0.0-rc.1 // Human error
require (
github.com/Masterminds/sprig/v3 v3.3.0
github.com/maypok86/otter v1.2.4
github.com/dunglas/mercure v0.21.4
github.com/e-dant/watcher/watcher-go v0.0.0-20251208164151-f88ec3b7e146
github.com/maypok86/otter/v2 v2.2.1
github.com/prometheus/client_golang v1.23.2
github.com/stretchr/testify v1.11.1
go.uber.org/zap v1.27.0
go.uber.org/zap/exp v0.3.0
golang.org/x/net v0.44.0
golang.org/x/net v0.48.0
)
require (
dario.cat/mergo v1.0.2 // indirect
github.com/Masterminds/goutils v1.1.1 // indirect
github.com/Masterminds/semver/v3 v3.4.0 // indirect
github.com/MauriceGit/skiplist v0.0.0-20211105230623-77f5c8d3e145 // indirect
github.com/RoaringBitmap/roaring/v2 v2.14.4 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/bits-and-blooms/bitset v1.24.4 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/dolthub/maphash v0.1.0 // indirect
github.com/gammazero/deque v1.1.0 // indirect
github.com/dunglas/skipfilter v1.0.0 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/fsnotify/fsnotify v1.9.0 // indirect
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
github.com/gofrs/uuid/v5 v5.4.0 // indirect
github.com/golang-jwt/jwt/v5 v5.3.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/gorilla/handlers v1.5.2 // indirect
github.com/gorilla/mux v1.8.1 // indirect
github.com/huandu/xstrings v1.5.0 // indirect
github.com/kylelemons/godebug v1.1.0 // indirect
github.com/mitchellh/copystructure v1.2.0 // indirect
github.com/mitchellh/reflectwalk v1.0.2 // indirect
github.com/mschoch/smat v0.2.0 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.66.1 // indirect
github.com/prometheus/procfs v0.17.0 // indirect
github.com/prometheus/common v0.67.4 // indirect
github.com/prometheus/procfs v0.19.2 // indirect
github.com/rogpeppe/go-internal v1.13.1 // indirect
github.com/rs/cors v1.11.1 // indirect
github.com/sagikazarmark/locafero v0.12.0 // indirect
github.com/shopspring/decimal v1.4.0 // indirect
github.com/spf13/cast v1.9.2 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.yaml.in/yaml/v2 v2.4.2 // indirect
golang.org/x/crypto v0.42.0 // indirect
golang.org/x/sys v0.36.0 // indirect
golang.org/x/text v0.29.0 // indirect
google.golang.org/protobuf v1.36.8 // indirect
github.com/spf13/afero v1.15.0 // indirect
github.com/spf13/cast v1.10.0 // indirect
github.com/spf13/pflag v1.0.10 // indirect
github.com/spf13/viper v1.21.0 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
github.com/unrolled/secure v1.17.0 // indirect
github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
go.etcd.io/bbolt v1.4.3 // indirect
go.yaml.in/yaml/v2 v2.4.3 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/crypto v0.46.0 // indirect
golang.org/x/sys v0.39.0 // indirect
golang.org/x/text v0.32.0 // indirect
google.golang.org/protobuf v1.36.11 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

104
go.sum
View File

@@ -6,76 +6,120 @@ github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1
github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
github.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe3tPhs=
github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0=
github.com/MauriceGit/skiplist v0.0.0-20211105230623-77f5c8d3e145 h1:1yw6O62BReQ+uA1oyk9XaQTvLhcoHWmoQAgXmDFXpIY=
github.com/MauriceGit/skiplist v0.0.0-20211105230623-77f5c8d3e145/go.mod h1:877WBceefKn14QwVVn4xRFUsHsZb9clICgdeTj4XsUg=
github.com/RoaringBitmap/roaring/v2 v2.14.4 h1:4aKySrrg9G/5oRtJ3TrZLObVqxgQ9f1znCRBwEwjuVw=
github.com/RoaringBitmap/roaring/v2 v2.14.4/go.mod h1:oMvV6omPWr+2ifRdeZvVJyaz+aoEUopyv5iH0u/+wbY=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bits-and-blooms/bitset v1.24.4 h1:95H15Og1clikBrKr/DuzMXkQzECs1M6hhoGXLwLQOZE=
github.com/bits-and-blooms/bitset v1.24.4/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dolthub/maphash v0.1.0 h1:bsQ7JsF4FkkWyrP3oCnFJgrCUAFbFf3kOl4L/QxPDyQ=
github.com/dolthub/maphash v0.1.0/go.mod h1:gkg4Ch4CdCDu5h6PMriVLawB7koZ+5ijb9puGMV50a4=
github.com/dunglas/mercure v0.21.4 h1:mXPXHfB+4cYfFFCRRDY198mfef5+MQcdCpUnAHBUW2M=
github.com/dunglas/mercure v0.21.4/go.mod h1:l/dglCjp/OQx8/quRyceRPx2hqZQ3CNviwoLMRQiJ/k=
github.com/dunglas/skipfilter v1.0.0 h1:JG9SgGg4n6BlFwuTYzb9RIqjH7PfwszvWehanrYWPF4=
github.com/dunglas/skipfilter v1.0.0/go.mod h1:ryhr8j7CAHSjzeN7wI6YEuwoArQ3OQmRqWWVCEAfb9w=
github.com/e-dant/watcher/watcher-go v0.0.0-20251208164151-f88ec3b7e146 h1:h3vVM6X45PK0mAk8NqiYNQGXTyhvXy1HQ5GhuQN4eeA=
github.com/e-dant/watcher/watcher-go v0.0.0-20251208164151-f88ec3b7e146/go.mod h1:sVUOkwtftoj71nnJRG2S0oWNfXFdKpz/M9vK0z06nmM=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/gammazero/deque v1.1.0 h1:OyiyReBbnEG2PP0Bnv1AASLIYvyKqIFN5xfl1t8oGLo=
github.com/gammazero/deque v1.1.0/go.mod h1:JVrR+Bj1NMQbPnYclvDlvSX0nVGReLrQZ0aUMuWLctg=
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/gofrs/uuid/v5 v5.4.0 h1:EfbpCTjqMuGyq5ZJwxqzn3Cbr2d0rUZU7v5ycAk/e/0=
github.com/gofrs/uuid/v5 v5.4.0/go.mod h1:CDOjlDMVAtN56jqyRUZh58JT31Tiw7/oQyEXZV+9bD8=
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/handlers v1.5.2 h1:cLTUSsNkgcwhgRqvCNmdbRWG0A3N4F+M2nWKdScwyEE=
github.com/gorilla/handlers v1.5.2/go.mod h1:dX+xVpaxdSw+q0Qek8SSsl3dfMk3jNddUkMzo0GtH0w=
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI=
github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/maypok86/otter v1.2.4 h1:HhW1Pq6VdJkmWwcZZq19BlEQkHtI8xgsQzBVXJU0nfc=
github.com/maypok86/otter v1.2.4/go.mod h1:mKLfoI7v1HOmQMwFgX4QkRk23mX6ge3RDvjdHOWG4R4=
github.com/maypok86/otter/v2 v2.2.1 h1:hnGssisMFkdisYcvQ8L019zpYQcdtPse+g0ps2i7cfI=
github.com/maypok86/otter/v2 v2.2.1/go.mod h1:1NKY9bY+kB5jwCXBJfE59u+zAwOt6C7ni1FTlFFMqVs=
github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
github.com/mschoch/smat v0.2.0 h1:8imxQsjDm8yFEAVBe7azKmKSgzSkZXDuKkSq9374khM=
github.com/mschoch/smat v0.2.0/go.mod h1:kc9mz7DoBKqDyiRL7VZN8KvXQMWeTaVnttLRXOlotKw=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs=
github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA=
github.com/prometheus/procfs v0.17.0 h1:FuLQ+05u4ZI+SS/w9+BWEM2TXiHKsUQ9TADiRH7DuK0=
github.com/prometheus/procfs v0.17.0/go.mod h1:oPQLaDAMRbA+u8H5Pbfq+dl3VDAvHxMUOVhe0wYB2zw=
github.com/prometheus/common v0.67.4 h1:yR3NqWO1/UyO1w2PhUvXlGQs/PtFmoveVO0KZ4+Lvsc=
github.com/prometheus/common v0.67.4/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI=
github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws=
github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw=
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA=
github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU=
github.com/sagikazarmark/locafero v0.12.0 h1:/NQhBAkUb4+fH1jivKHWusDYFjMOOKU88eegjfxfHb4=
github.com/sagikazarmark/locafero v0.12.0/go.mod h1:sZh36u/YSZ918v0Io+U9ogLYQJ9tLLBmM4eneO6WwsI=
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
github.com/spf13/cast v1.9.2 h1:SsGfm7M8QOFtEzumm7UZrZdLLquNdzFYfIbEXntcFbE=
github.com/spf13/cast v1.9.2/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=
github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU=
github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
github.com/unrolled/secure v1.17.0 h1:Io7ifFgo99Bnh0J7+Q+qcMzWM6kaDPCA5FroFZEdbWU=
github.com/unrolled/secure v1.17.0/go.mod h1:BmF5hyM6tXczk3MpQkFf1hpKSRqCyhqcbiQtiAF7+40=
github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4=
github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4=
go.etcd.io/bbolt v1.4.3 h1:dEadXpI6G79deX5prL3QRNP6JB8UxVkqo4UPnHaNXJo=
go.etcd.io/bbolt v1.4.3/go.mod h1:tKQlpPaYCVFctUIgFKFnAlvbmB3tpy1vkTnDWohtc0E=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
go.uber.org/zap/exp v0.3.0 h1:6JYzdifzYkGmTdRR59oYH+Ng7k49H9qVpWwNSsGJj3U=
go.uber.org/zap/exp v0.3.0/go.mod h1:5I384qq7XGxYyByIhHm6jg5CHkGY0nsTfbDLgDDlgJQ=
go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI=
go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU=
golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI=
golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8=
golang.org/x/net v0.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I=
golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY=
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=
google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc=
google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=

44
hotreload.go Normal file
View File

@@ -0,0 +1,44 @@
//go:build !nomercure && !nowatcher
package frankenphp
import (
"encoding/json"
"log/slog"
"github.com/dunglas/frankenphp/internal/watcher"
"github.com/dunglas/mercure"
watcherGo "github.com/e-dant/watcher/watcher-go"
)
// WithHotReload sets files to watch for file changes to trigger a hot reload update.
func WithHotReload(topic string, hub *mercure.Hub, patterns []string) Option {
return func(o *opt) error {
o.hotReload = append(o.hotReload, &watcher.PatternGroup{
Patterns: patterns,
Callback: func(events []*watcherGo.Event) {
// Wait for workers to restart before sending the update
go func() {
data, err := json.Marshal(events)
if err != nil {
if globalLogger.Enabled(globalCtx, slog.LevelError) {
globalLogger.LogAttrs(globalCtx, slog.LevelError, "error marshaling watcher events", slog.Any("error", err))
}
return
}
if err := hub.Publish(globalCtx, &mercure.Update{
Topics: []string{topic},
Event: mercure.Event{Data: string(data)},
Debug: globalLogger.Enabled(globalCtx, slog.LevelDebug),
}); err != nil && globalLogger.Enabled(globalCtx, slog.LevelError) {
globalLogger.LogAttrs(globalCtx, slog.LevelError, "error publishing hot reloading Mercure update", slog.Any("error", err))
}
}()
},
})
return nil
}
}

View File

@@ -2,6 +2,11 @@
set -e
SUDO=""
if [ "$(id -u)" -ne 0 ]; then
SUDO="sudo"
fi
if [ -z "${BIN_DIR}" ]; then
BIN_DIR=$(pwd)
fi
@@ -13,6 +18,11 @@ OS=$(uname -s)
ARCH=$(uname -m)
GNU=""
if ! command -v curl >/dev/null 2>&1; then
echo "Please install curl to download FrankenPHP"
exit 1
fi
if type "tput" >/dev/null 2>&1; then
bold=$(tput bold || true)
italic=$(tput sitm || true)
@@ -21,6 +31,50 @@ fi
case ${OS} in
Linux*)
if [ "${ARCH}" = "aarch64" ] || [ "${ARCH}" = "x86_64" ]; then
if command -v dnf >/dev/null 2>&1; then
echo "📦 Detected dnf. Installing FrankenPHP from RPM repository..."
if [ -n "${SUDO}" ]; then
echo "❗ Enter your password to grant sudo powers for package installation"
${SUDO} -v || true
fi
${SUDO} dnf -y install https://rpm.henderkes.com/static-php-1-0.noarch.rpm
${SUDO} dnf -y module enable php-zts:static-8.4 || true
${SUDO} dnf -y install frankenphp
echo
echo "🥳 FrankenPHP installed to ${italic}/usr/bin/frankenphp${normal} successfully."
echo "❗ The systemd service uses the Caddyfile in ${italic}/etc/frankenphp/Caddyfile${normal}"
echo "❗ Your php.ini is found in ${italic}/etc/php-zts/php.ini${normal}"
echo
echo "⭐ If you like FrankenPHP, please give it a star on GitHub: ${italic}https://github.com/php/frankenphp${normal}"
exit 0
fi
if command -v apt >/dev/null 2>&1 || command -v apt-get >/dev/null 2>&1; then
echo "📦 Detected apt. Installing FrankenPHP from DEB repository..."
if [ -n "${SUDO}" ]; then
echo "❗ Enter your password to grant sudo powers for package installation"
${SUDO} -v || true
fi
${SUDO} sh -c 'curl -fsSL https://key.henderkes.com/static-php.gpg -o /usr/share/keyrings/static-php.gpg'
${SUDO} sh -c 'echo "deb [signed-by=/usr/share/keyrings/static-php.gpg] https://deb.henderkes.com/ stable main" > /etc/apt/sources.list.d/static-php.list'
if command -v apt >/dev/null 2>&1; then
${SUDO} apt update
${SUDO} apt -y install frankenphp
else
${SUDO} apt-get update
${SUDO} apt-get -y install frankenphp
fi
echo
echo "🥳 FrankenPHP installed to ${italic}/usr/bin/frankenphp${normal} successfully."
echo "❗ The systemd service uses the Caddyfile in ${italic}/etc/frankenphp/Caddyfile${normal}"
echo "❗ Your php.ini is found in ${italic}/etc/php-zts/php.ini${normal}"
echo
echo "⭐ If you like FrankenPHP, please give it a star on GitHub: ${italic}https://github.com/php/frankenphp${normal}"
exit 0
fi
fi
case ${ARCH} in
aarch64)
THE_ARCH_BIN="frankenphp-linux-aarch64"
@@ -58,12 +112,11 @@ Windows | MINGW64_NT*)
esac
if [ -z "${THE_ARCH_BIN}" ]; then
echo "FrankenPHP is not supported on ${OS} and ${ARCH}"
echo "Precompiled binaries are not available for ${ARCH}-${OS}"
echo "❗ You can compile from sources by following the documentation at: https://frankenphp.dev/docs/compile/"
exit 1
fi
SUDO=""
echo "📦 Downloading ${bold}FrankenPHP${normal} for ${OS}${GNU} (${ARCH}):"
# check if $DEST is writable and suppress an error message
@@ -75,20 +128,27 @@ if [ $? -eq 1 ]; then
SUDO="sudo"
fi
if type "curl" >/dev/null 2>&1; then
curl -L --progress-bar "https://github.com/php/frankenphp/releases/latest/download/${THE_ARCH_BIN}" -o "${DEST}"
elif type "wget" >/dev/null 2>&1; then
${SUDO} wget "https://github.com/php/frankenphp/releases/latest/download/${THE_ARCH_BIN}" -O "${DEST}"
else
echo "❗ Please install ${italic}curl${normal} or ${italic}wget${normal} to download FrankenPHP"
exit 1
fi
curl -L --progress-bar "https://github.com/php/frankenphp/releases/latest/download/${THE_ARCH_BIN}" -o "${DEST}"
${SUDO} chmod +x "${DEST}"
# Allow binding to ports 80/443 without running as root (if setcap is available)
if command -v setcap >/dev/null 2>&1; then
${SUDO} setcap 'cap_net_bind_service=+ep' "${DEST}" || true
else
echo "❗ install setcap (e.g. libcap2-bin) to allow FrankenPHP to bind to ports 80/443 without root:"
echo " ${bold}sudo setcap 'cap_net_bind_service=+ep' \"${DEST}\"${normal}"
fi
echo
echo "🥳 FrankenPHP downloaded successfully to ${italic}${DEST}${normal}"
echo "🔧 Move the binary to ${italic}/usr/local/bin/${normal} or another directory in your ${italic}PATH${normal} to use it globally:"
echo " ${bold}sudo mv ${DEST} /usr/local/bin/${normal}"
echo "❗ It uses ${italic}/etc/frankenphp/php.ini${normal} if found."
case ":$PATH:" in
*":$DEST:"*) ;;
*)
echo "🔧 Move the binary to ${italic}/usr/local/bin/${normal} or another directory in your ${italic}PATH${normal} to use it globally:"
echo " ${bold}sudo mv ${DEST} /usr/local/bin/${normal}"
;;
esac
echo
echo "⭐ If you like FrankenPHP, please give it a star on GitHub: ${italic}https://github.com/php/frankenphp${normal}"

View File

@@ -2,6 +2,7 @@ package extgen
import (
"fmt"
"log"
"os"
"os/exec"
"path/filepath"
@@ -25,8 +26,10 @@ func (ag *arginfoGenerator) generate() error {
stubFile := ag.generator.BaseName + ".stub.php"
cmd := exec.Command("php", genStubPath, filepath.Join(ag.generator.BuildDir, stubFile))
if err := cmd.Run(); err != nil {
return fmt.Errorf("running gen_stub script: %w", err)
output, err := cmd.CombinedOutput()
if err != nil {
log.Print("gen_stub.php output:\n", string(output))
return fmt.Errorf("running gen_stub script: %w\nOutput: %s", err, string(output))
}
return ag.fixArginfoFile(stubFile)

View File

@@ -58,6 +58,7 @@ func (cg *cFileGenerator) buildContent() (string, error) {
func (cg *cFileGenerator) getTemplateContent() (string, error) {
funcMap := sprig.FuncMap()
funcMap["namespacedClassName"] = NamespacedName
funcMap["cString"] = escapeCString
tmpl := template.Must(template.New("cfile").Funcs(funcMap).Parse(cFileContent))
@@ -74,3 +75,8 @@ func (cg *cFileGenerator) getTemplateContent() (string, error) {
return buf.String(), nil
}
// escapeCString escapes backslashes for C string literals
func escapeCString(s string) string {
return strings.ReplaceAll(s, `\`, `\\`)
}

View File

@@ -129,3 +129,212 @@ func TestCFileGenerationWithoutNamespace(t *testing.T) {
expectedCall := "register_class_MySuperClass()"
require.Contains(t, contentResult, expectedCall, "C file should not contain the standard function call")
}
func TestCFileGenerationWithNamespacedConstants(t *testing.T) {
tests := []struct {
name string
namespace string
constants []phpConstant
contains []string
}{
{
name: "integer constant with namespace",
namespace: `Go\Extension`,
constants: []phpConstant{
{Name: "TEST_INT", Value: "42", PhpType: phpInt},
},
contains: []string{
`REGISTER_NS_LONG_CONSTANT("Go\\Extension", "TEST_INT", 42, CONST_CS | CONST_PERSISTENT);`,
},
},
{
name: "string constant with namespace",
namespace: `Go\Extension`,
constants: []phpConstant{
{Name: "TEST_STRING", Value: `"hello"`, PhpType: phpString},
},
contains: []string{
`REGISTER_NS_STRING_CONSTANT("Go\\Extension", "TEST_STRING", "hello", CONST_CS | CONST_PERSISTENT);`,
},
},
{
name: "float constant with namespace",
namespace: `Go\Extension`,
constants: []phpConstant{
{Name: "TEST_FLOAT", Value: "3.14", PhpType: phpFloat},
},
contains: []string{
`REGISTER_NS_DOUBLE_CONSTANT("Go\\Extension", "TEST_FLOAT", 3.14, CONST_CS | CONST_PERSISTENT);`,
},
},
{
name: "bool constant with namespace",
namespace: `Go\Extension`,
constants: []phpConstant{
{Name: "TEST_BOOL", Value: "true", PhpType: phpBool},
},
contains: []string{
`REGISTER_NS_BOOL_CONSTANT("Go\\Extension", "TEST_BOOL", true, CONST_CS | CONST_PERSISTENT);`,
},
},
{
name: "iota constant with namespace",
namespace: `Go\Extension`,
constants: []phpConstant{
{Name: "STATUS_OK", Value: "STATUS_OK", PhpType: phpInt, IsIota: true},
},
contains: []string{
`REGISTER_NS_LONG_CONSTANT("Go\\Extension", "STATUS_OK", STATUS_OK, CONST_CS | CONST_PERSISTENT);`,
},
},
{
name: "multiple constants with deep namespace",
namespace: `My\Deep\Namespace`,
constants: []phpConstant{
{Name: "CONST_INT", Value: "100", PhpType: phpInt},
{Name: "CONST_STR", Value: `"value"`, PhpType: phpString},
{Name: "CONST_FLOAT", Value: "1.5", PhpType: phpFloat},
},
contains: []string{
`REGISTER_NS_LONG_CONSTANT("My\\Deep\\Namespace", "CONST_INT", 100, CONST_CS | CONST_PERSISTENT);`,
`REGISTER_NS_STRING_CONSTANT("My\\Deep\\Namespace", "CONST_STR", "value", CONST_CS | CONST_PERSISTENT);`,
`REGISTER_NS_DOUBLE_CONSTANT("My\\Deep\\Namespace", "CONST_FLOAT", 1.5, CONST_CS | CONST_PERSISTENT);`,
},
},
{
name: "single level namespace",
namespace: `TestNamespace`,
constants: []phpConstant{
{Name: "MY_CONST", Value: "999", PhpType: phpInt},
},
contains: []string{
`REGISTER_NS_LONG_CONSTANT("TestNamespace", "MY_CONST", 999, CONST_CS | CONST_PERSISTENT);`,
},
},
{
name: "namespace with trailing backslash",
namespace: `TestIntegration\Extension`,
constants: []phpConstant{
{Name: "VERSION", Value: `"1.0.0"`, PhpType: phpString},
},
contains: []string{
`REGISTER_NS_STRING_CONSTANT("TestIntegration\\Extension", "VERSION", "1.0.0", CONST_CS | CONST_PERSISTENT);`,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
generator := &Generator{
BaseName: "test_ext",
Namespace: tt.namespace,
Constants: tt.constants,
}
cGen := cFileGenerator{generator: generator}
content, err := cGen.buildContent()
require.NoError(t, err, "Failed to build C file content")
for _, expected := range tt.contains {
assert.Contains(t, content, expected, "Generated C content should contain '%s'", expected)
}
})
}
}
func TestCFileGenerationWithoutNamespacedConstants(t *testing.T) {
tests := []struct {
name string
namespace string
constants []phpConstant
contains []string
}{
{
name: "integer constant without namespace",
namespace: "",
constants: []phpConstant{
{Name: "GLOBAL_INT", Value: "42", PhpType: phpInt},
},
contains: []string{
`REGISTER_LONG_CONSTANT("GLOBAL_INT", 42, CONST_CS | CONST_PERSISTENT);`,
},
},
{
name: "string constant without namespace",
namespace: "",
constants: []phpConstant{
{Name: "GLOBAL_STRING", Value: `"test"`, PhpType: phpString},
},
contains: []string{
`REGISTER_STRING_CONSTANT("GLOBAL_STRING", "test", CONST_CS | CONST_PERSISTENT);`,
},
},
{
name: "float constant without namespace",
namespace: "",
constants: []phpConstant{
{Name: "GLOBAL_FLOAT", Value: "2.71", PhpType: phpFloat},
},
contains: []string{
`REGISTER_DOUBLE_CONSTANT("GLOBAL_FLOAT", 2.71, CONST_CS | CONST_PERSISTENT);`,
},
},
{
name: "bool constant without namespace",
namespace: "",
constants: []phpConstant{
{Name: "GLOBAL_BOOL", Value: "false", PhpType: phpBool},
},
contains: []string{
`REGISTER_BOOL_CONSTANT("GLOBAL_BOOL", false, CONST_CS | CONST_PERSISTENT);`,
},
},
{
name: "iota constant without namespace",
namespace: "",
constants: []phpConstant{
{Name: "ERROR_CODE", Value: "ERROR_CODE", PhpType: phpInt, IsIota: true},
},
contains: []string{
`REGISTER_LONG_CONSTANT("ERROR_CODE", ERROR_CODE, CONST_CS | CONST_PERSISTENT);`,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
generator := &Generator{
BaseName: "test_ext",
Namespace: tt.namespace,
Constants: tt.constants,
}
cGen := cFileGenerator{generator: generator}
content, err := cGen.buildContent()
require.NoError(t, err, "Failed to build C file content")
for _, expected := range tt.contains {
assert.Contains(t, content, expected, "Generated C content should contain '%s'", expected)
}
assert.NotContains(t, content, "REGISTER_NS_", "Content should NOT contain namespaced constant macros when namespace is empty")
})
}
}
func TestCFileTemplateFunctionMapCString(t *testing.T) {
generator := &Generator{
BaseName: "test_ext",
Namespace: `My\Namespace\Test`,
Constants: []phpConstant{
{Name: "MY_CONST", Value: "123", PhpType: phpInt},
},
}
cGen := cFileGenerator{generator: generator}
content, err := cGen.getTemplateContent()
require.NoError(t, err, "Failed to get template content")
assert.Contains(t, content, `"My\\Namespace\\Test"`, "Template should properly escape namespace backslashes using cString filter")
assert.NotContains(t, content, `"My\Namespace\Test"`, "Template should not contain unescaped namespace (single backslashes)")
}

View File

@@ -185,3 +185,32 @@ func TestCFile_PHP_METHOD_Integration(t *testing.T) {
require.NotContains(t, fullContent, old, "Did not expect to find old declaration %q in full C file content", old)
}
}
func TestCFile_ClassMethodStringReturn(t *testing.T) {
generator := &Generator{
BaseName: "test_extension",
Classes: []phpClass{
{
Name: "TestClass",
GoStruct: "TestClass",
Methods: []phpClassMethod{
{
Name: "getString",
PhpName: "getString",
ReturnType: "string",
ClassName: "TestClass",
},
},
},
},
BuildDir: t.TempDir(),
}
cFileGen := cFileGenerator{generator: generator}
content, err := cFileGen.getTemplateContent()
require.NoError(t, err)
require.Contains(t, content, "if (result)", "Expected NULL check for string return")
require.Contains(t, content, "RETURN_STR(result)", "Expected RETURN_STR macro")
require.Contains(t, content, "RETURN_EMPTY_STRING()", "Expected RETURN_EMPTY_STRING fallback")
}

View File

@@ -459,3 +459,74 @@ func TestCFileTemplateErrorHandling(t *testing.T) {
_, err := cGen.getTemplateContent()
assert.NoError(t, err, "getTemplateContent() should not fail with valid template")
}
func TestEscapeCString(t *testing.T) {
tests := []struct {
name string
input string
expected string
}{
{
name: "simple namespace with single backslash",
input: `Go\Extension`,
expected: `Go\\Extension`,
},
{
name: "namespace with multiple backslashes",
input: `My\Deep\Namespace`,
expected: `My\\Deep\\Namespace`,
},
{
name: "complex nested namespace",
input: `TestIntegration\Extension\Module`,
expected: `TestIntegration\\Extension\\Module`,
},
{
name: "empty string",
input: "",
expected: "",
},
{
name: "single backslash",
input: `\`,
expected: `\\`,
},
{
name: "multiple consecutive backslashes",
input: `\\\`,
expected: `\\\\\\`,
},
{
name: "string without backslashes",
input: "TestNamespace",
expected: "TestNamespace",
},
{
name: "leading backslash",
input: `\Leading`,
expected: `\\Leading`,
},
{
name: "trailing backslash",
input: `Trailing\`,
expected: `Trailing\\`,
},
{
name: "mixed alphanumeric with backslashes",
input: `Path123\To456\File789`,
expected: `Path123\\To456\\File789`,
},
{
name: "unicode characters with backslashes",
input: `Namespace\Über\Test`,
expected: `Namespace\\Über\\Test`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := escapeCString(tt.input)
assert.Equal(t, tt.expected, result, "escapeCString(%q) should return %q, got %q", tt.input, tt.expected, result)
})
}
}

View File

@@ -50,10 +50,6 @@ func (g *Generator) Generate() error {
}
func (g *Generator) setupBuildDirectory() error {
if err := os.RemoveAll(g.BuildDir); err != nil {
return fmt.Errorf("removing build directory: %w", err)
}
return os.MkdirAll(g.BuildDir, 0755)
}

View File

@@ -4,6 +4,7 @@ import (
"bytes"
_ "embed"
"fmt"
"os"
"path/filepath"
"text/template"
@@ -30,6 +31,16 @@ type goTemplateData struct {
func (gg *GoFileGenerator) generate() error {
filename := filepath.Join(gg.generator.BuildDir, gg.generator.BaseName+".go")
if _, err := os.Stat(filename); err == nil {
backupFilename := filename + ".bak"
if err := os.Rename(filename, backupFilename); err != nil {
return fmt.Errorf("backing up existing Go file: %w", err)
}
gg.generator.SourceFile = backupFilename
}
content, err := gg.buildContent()
if err != nil {
return fmt.Errorf("building Go file content: %w", err)
@@ -47,7 +58,7 @@ func (gg *GoFileGenerator) buildContent() (string, error) {
filteredImports := make([]string, 0, len(imports))
for _, imp := range imports {
if imp != `"C"` && imp != `"unsafe"` && imp != `"github.com/dunglas/frankenphp"` {
if imp != `"C"` && imp != `"unsafe"` && imp != `"github.com/dunglas/frankenphp"` && imp != `"runtime/cgo"` {
filteredImports = append(filteredImports, imp)
}
}
@@ -55,6 +66,19 @@ func (gg *GoFileGenerator) buildContent() (string, error) {
classes := make([]phpClass, len(gg.generator.Classes))
copy(classes, gg.generator.Classes)
if len(classes) > 0 {
hasCgo := false
for _, imp := range imports {
if imp == `"runtime/cgo"` {
hasCgo = true
break
}
}
if !hasCgo {
filteredImports = append(filteredImports, `"runtime/cgo"`)
}
}
templateContent, err := gg.getTemplateContent(goTemplateData{
PackageName: SanitizePackageName(gg.generator.BaseName),
BaseName: gg.generator.BaseName,
@@ -104,14 +128,15 @@ type GoParameter struct {
Type string
}
var phpToGoTypeMap = map[phpType]string{
phpString: "string",
phpInt: "int64",
phpFloat: "float64",
phpBool: "bool",
phpArray: "*frankenphp.Array",
phpMixed: "any",
phpVoid: "",
var phpToGoTypeMap= map[phpType]string{
phpString: "string",
phpInt: "int64",
phpFloat: "float64",
phpBool: "bool",
phpArray: "*frankenphp.Array",
phpMixed: "any",
phpVoid: "",
phpCallable: "*C.zval",
}
func (gg *GoFileGenerator) phpTypeToGoType(phpT phpType) string {

View File

@@ -703,6 +703,125 @@ func testGoFileExportedFunctions(t *testing.T, content string, functions []phpFu
}
}
func TestGoFileGenerator_MethodWrapperWithCallableParams(t *testing.T) {
tmpDir := t.TempDir()
sourceContent := `package main
import "C"
//export_php:class CallableClass
type CallableStruct struct{}
//export_php:method CallableClass::processCallback(callable $callback): string
func (cs *CallableStruct) ProcessCallback(callback *C.zval) string {
return "processed"
}
//export_php:method CallableClass::processOptionalCallback(?callable $callback): string
func (cs *CallableStruct) ProcessOptionalCallback(callback *C.zval) string {
return "processed_optional"
}`
sourceFile := filepath.Join(tmpDir, "test.go")
require.NoError(t, os.WriteFile(sourceFile, []byte(sourceContent), 0644))
methods := []phpClassMethod{
{
Name: "ProcessCallback",
PhpName: "processCallback",
ClassName: "CallableClass",
Signature: "processCallback(callable $callback): string",
ReturnType: phpString,
Params: []phpParameter{
{Name: "callback", PhpType: phpCallable, IsNullable: false},
},
GoFunction: `func (cs *CallableStruct) ProcessCallback(callback *C.zval) string {
return "processed"
}`,
},
{
Name: "ProcessOptionalCallback",
PhpName: "processOptionalCallback",
ClassName: "CallableClass",
Signature: "processOptionalCallback(?callable $callback): string",
ReturnType: phpString,
Params: []phpParameter{
{Name: "callback", PhpType: phpCallable, IsNullable: true},
},
GoFunction: `func (cs *CallableStruct) ProcessOptionalCallback(callback *C.zval) string {
return "processed_optional"
}`,
},
}
classes := []phpClass{
{
Name: "CallableClass",
GoStruct: "CallableStruct",
Methods: methods,
},
}
generator := &Generator{
BaseName: "callable_test",
SourceFile: sourceFile,
Classes: classes,
BuildDir: tmpDir,
}
goGen := GoFileGenerator{generator}
content, err := goGen.buildContent()
require.NoError(t, err)
expectedCallableWrapperSignature := "func ProcessCallback_wrapper(handle C.uintptr_t, callback *C.zval) unsafe.Pointer"
assert.Contains(t, content, expectedCallableWrapperSignature, "Generated content should contain callable wrapper signature: %s", expectedCallableWrapperSignature)
expectedOptionalCallableWrapperSignature := "func ProcessOptionalCallback_wrapper(handle C.uintptr_t, callback *C.zval) unsafe.Pointer"
assert.Contains(t, content, expectedOptionalCallableWrapperSignature, "Generated content should contain optional callable wrapper signature: %s", expectedOptionalCallableWrapperSignature)
expectedCallableCall := "structObj.ProcessCallback(callback)"
assert.Contains(t, content, expectedCallableCall, "Generated content should contain callable method call: %s", expectedCallableCall)
expectedOptionalCallableCall := "structObj.ProcessOptionalCallback(callback)"
assert.Contains(t, content, expectedOptionalCallableCall, "Generated content should contain optional callable method call: %s", expectedOptionalCallableCall)
assert.Contains(t, content, "//export ProcessCallback_wrapper", "Generated content should contain ProcessCallback export directive")
assert.Contains(t, content, "//export ProcessOptionalCallback_wrapper", "Generated content should contain ProcessOptionalCallback export directive")
}
func TestGoFileGenerator_phpTypeToGoType(t *testing.T) {
generator := &Generator{}
goGen := GoFileGenerator{generator}
tests := []struct {
phpType phpType
expected string
}{
{phpString, "string"},
{phpInt, "int64"},
{phpFloat, "float64"},
{phpBool, "bool"},
{phpArray, "*frankenphp.Array"},
{phpMixed, "any"},
{phpVoid, ""},
{phpCallable, "*C.zval"},
}
for _, tt := range tests {
t.Run(string(tt.phpType), func(t *testing.T) {
result := goGen.phpTypeToGoType(tt.phpType)
assert.Equal(t, tt.expected, result, "phpTypeToGoType(%s) should return %s", tt.phpType, tt.expected)
})
}
t.Run("unknown_type", func(t *testing.T) {
unknownType := phpType("unknown")
result := goGen.phpTypeToGoType(unknownType)
assert.Equal(t, "any", result, "phpTypeToGoType should fallback to interface{} for unknown types")
})
}
func testGoFileInternalFunctions(t *testing.T, content string) {
internalIndicators := []string{
"func internalHelper",

View File

@@ -0,0 +1,793 @@
//go:build integration
package extgen
import (
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
const (
testModuleName = "github.com/frankenphp/test-extension"
)
type IntegrationTestSuite struct {
tempDir string
genStubScript string
xcaddyPath string
frankenphpPath string
phpConfigPath string
t *testing.T
}
func setupTest(t *testing.T) *IntegrationTestSuite {
t.Helper()
suite := &IntegrationTestSuite{t: t}
suite.genStubScript = os.Getenv("GEN_STUB_SCRIPT")
if suite.genStubScript == "" {
suite.genStubScript = "/usr/local/src/php/build/gen_stub.php"
}
if _, err := os.Stat(suite.genStubScript); os.IsNotExist(err) {
t.Error("GEN_STUB_SCRIPT not found. Integration tests require PHP sources. Set GEN_STUB_SCRIPT environment variable.")
}
xcaddyPath, err := exec.LookPath("xcaddy")
if err != nil {
t.Error("xcaddy not found in PATH. Integration tests require xcaddy to build FrankenPHP.")
}
suite.xcaddyPath = xcaddyPath
phpConfigPath, err := exec.LookPath("php-config")
if err != nil {
t.Error("php-config not found in PATH. Integration tests require PHP development headers.")
}
suite.phpConfigPath = phpConfigPath
tempDir := t.TempDir()
suite.tempDir = tempDir
return suite
}
func (s *IntegrationTestSuite) createGoModule(sourceFile string) (string, error) {
s.t.Helper()
moduleDir := filepath.Join(s.tempDir, "module")
if err := os.MkdirAll(moduleDir, 0o755); err != nil {
return "", fmt.Errorf("failed to create module directory: %w", err)
}
// Get project root for replace directive
projectRoot, err := filepath.Abs(filepath.Join("..", ".."))
if err != nil {
return "", fmt.Errorf("failed to get project root: %w", err)
}
goModContent := fmt.Sprintf(`module %s
go 1.25
require github.com/dunglas/frankenphp v0.0.0
replace github.com/dunglas/frankenphp => %s
`, testModuleName, projectRoot)
if err := os.WriteFile(filepath.Join(moduleDir, "go.mod"), []byte(goModContent), 0o644); err != nil {
return "", fmt.Errorf("failed to create go.mod: %w", err)
}
sourceContent, err := os.ReadFile(sourceFile)
if err != nil {
return "", fmt.Errorf("failed to read source file: %w", err)
}
targetFile := filepath.Join(moduleDir, filepath.Base(sourceFile))
if err := os.WriteFile(targetFile, sourceContent, 0o644); err != nil {
return "", fmt.Errorf("failed to write source file: %w", err)
}
return targetFile, nil
}
func (s *IntegrationTestSuite) runExtensionInit(sourceFile string) error {
s.t.Helper()
os.Setenv("GEN_STUB_SCRIPT", s.genStubScript)
baseName := SanitizePackageName(strings.TrimSuffix(filepath.Base(sourceFile), ".go"))
generator := Generator{
BaseName: baseName,
SourceFile: sourceFile,
BuildDir: filepath.Dir(sourceFile),
}
if err := generator.Generate(); err != nil {
return fmt.Errorf("generation failed: %w", err)
}
return nil
}
// cleanupGeneratedFiles removes generated files from the original source directory
func (s *IntegrationTestSuite) cleanupGeneratedFiles(originalSourceFile string) {
s.t.Helper()
sourceDir := filepath.Dir(originalSourceFile)
baseName := SanitizePackageName(strings.TrimSuffix(filepath.Base(originalSourceFile), ".go"))
generatedFiles := []string{
baseName + ".stub.php",
baseName + "_arginfo.h",
baseName + ".h",
baseName + ".c",
baseName + ".go",
"README.md",
}
for _, file := range generatedFiles {
fullPath := filepath.Join(sourceDir, file)
if _, err := os.Stat(fullPath); err == nil {
os.Remove(fullPath)
}
}
}
// compileFrankenPHP compiles FrankenPHP with the generated extension
func (s *IntegrationTestSuite) compileFrankenPHP(moduleDir string) (string, error) {
s.t.Helper()
projectRoot, err := filepath.Abs(filepath.Join("..", ".."))
if err != nil {
return "", fmt.Errorf("failed to get project root: %w", err)
}
cflags, err := exec.Command(s.phpConfigPath, "--includes").Output()
if err != nil {
return "", fmt.Errorf("failed to get PHP includes: %w", err)
}
ldflags, err := exec.Command(s.phpConfigPath, "--ldflags").Output()
if err != nil {
return "", fmt.Errorf("failed to get PHP ldflags: %w", err)
}
libs, err := exec.Command(s.phpConfigPath, "--libs").Output()
if err != nil {
return "", fmt.Errorf("failed to get PHP libs: %w", err)
}
cgoCflags := strings.TrimSpace(string(cflags))
cgoLdflags := strings.TrimSpace(string(ldflags)) + " " + strings.TrimSpace(string(libs))
outputBinary := filepath.Join(s.tempDir, "frankenphp")
cmd := exec.Command(
s.xcaddyPath,
"build",
"--output", outputBinary,
"--with", "github.com/dunglas/frankenphp="+projectRoot,
"--with", "github.com/dunglas/frankenphp/caddy="+projectRoot+"/caddy",
"--with", testModuleName+"="+moduleDir,
)
cmd.Env = append(os.Environ(),
"CGO_ENABLED=1",
"CGO_CFLAGS="+cgoCflags,
"CGO_LDFLAGS="+cgoLdflags,
fmt.Sprintf("XCADDY_GO_BUILD_FLAGS=-ldflags='-w -s' -tags=nobadger,nomysql,nopgx,nowatcher"),
)
cmd.Dir = s.tempDir
output, err := cmd.CombinedOutput()
if err != nil {
return "", fmt.Errorf("xcaddy build failed: %w\nOutput: %s", err, string(output))
}
s.frankenphpPath = outputBinary
return outputBinary, nil
}
func (s *IntegrationTestSuite) runPHPCode(phpCode string) (string, error) {
s.t.Helper()
if s.frankenphpPath == "" {
return "", fmt.Errorf("FrankenPHP not compiled yet")
}
phpFile := filepath.Join(s.tempDir, "test.php")
if err := os.WriteFile(phpFile, []byte(phpCode), 0o644); err != nil {
return "", fmt.Errorf("failed to create PHP file: %w", err)
}
cmd := exec.Command(s.frankenphpPath, "php-cli", phpFile)
output, err := cmd.CombinedOutput()
if err != nil {
return "", fmt.Errorf("PHP execution failed: %w\nOutput: %s", err, string(output))
}
return string(output), nil
}
// verifyPHPSymbols checks if PHP can find the exposed functions, classes, and constants
func (s *IntegrationTestSuite) verifyPHPSymbols(functions []string, classes []string, constants []string) error {
s.t.Helper()
var checks []string
for _, fn := range functions {
checks = append(checks, fmt.Sprintf("if (!function_exists('%s')) { echo 'MISSING_FUNCTION: %s'; exit(1); }", fn, fn))
}
for _, cls := range classes {
checks = append(checks, fmt.Sprintf("if (!class_exists('%s')) { echo 'MISSING_CLASS: %s'; exit(1); }", cls, cls))
}
for _, cnst := range constants {
checks = append(checks, fmt.Sprintf("if (!defined('%s')) { echo 'MISSING_CONSTANT: %s'; exit(1); }", cnst, cnst))
}
checks = append(checks, "echo 'OK';")
phpCode := "<?php\n" + strings.Join(checks, "\n")
output, err := s.runPHPCode(phpCode)
if err != nil {
return err
}
if !strings.Contains(output, "OK") {
return fmt.Errorf("symbol verification failed: %s", output)
}
return nil
}
func (s *IntegrationTestSuite) verifyFunctionBehavior(phpCode string, expectedOutput string) error {
s.t.Helper()
output, err := s.runPHPCode(phpCode)
if err != nil {
return err
}
if !strings.Contains(output, expectedOutput) {
return fmt.Errorf("unexpected output.\nExpected to contain: %q\nGot: %q", expectedOutput, output)
}
return nil
}
func TestBasicFunction(t *testing.T) {
suite := setupTest(t)
sourceFile := filepath.Join("..", "..", "testdata", "integration", "basic_function.go")
sourceFile, err := filepath.Abs(sourceFile)
require.NoError(t, err)
defer suite.cleanupGeneratedFiles(sourceFile)
targetFile, err := suite.createGoModule(sourceFile)
require.NoError(t, err)
err = suite.runExtensionInit(targetFile)
require.NoError(t, err, "extension-init should succeed")
baseDir := filepath.Dir(targetFile)
baseName := strings.TrimSuffix(filepath.Base(targetFile), ".go")
expectedFiles := []string{
baseName + ".stub.php",
baseName + "_arginfo.h",
baseName + ".h",
baseName + ".c",
baseName + ".go",
"README.md",
}
for _, file := range expectedFiles {
fullPath := filepath.Join(baseDir, file)
assert.FileExists(t, fullPath, "Generated file should exist: %s", file)
}
_, err = suite.compileFrankenPHP(filepath.Dir(targetFile))
require.NoError(t, err, "FrankenPHP compilation should succeed")
err = suite.verifyPHPSymbols(
[]string{"test_uppercase", "test_add_numbers", "test_multiply", "test_is_enabled"},
[]string{},
[]string{},
)
require.NoError(t, err, "all functions should be accessible from PHP")
err = suite.verifyFunctionBehavior(`<?php
$result = test_uppercase("hello world");
if ($result !== "HELLO WORLD") {
echo "FAIL: test_uppercase expected 'HELLO WORLD', got '$result'";
exit(1);
}
$result = test_uppercase("");
if ($result !== "") {
echo "FAIL: test_uppercase with empty string expected '', got '$result'";
exit(1);
}
$sum = test_add_numbers(5, 7);
if ($sum !== 12) {
echo "FAIL: test_add_numbers(5, 7) expected 12, got $sum";
exit(1);
}
$result = test_is_enabled(true);
if ($result !== false) {
echo "FAIL: test_is_enabled(true) expected false, got " . ($result ? "true" : "false");
exit(1);
}
$result = test_is_enabled(false);
if ($result !== true) {
echo "FAIL: test_is_enabled(false) expected true, got " . ($result ? "true" : "false");
exit(1);
}
echo "OK";
`, "OK")
require.NoError(t, err, "all function calls should work correctly")
}
func TestClassMethodsIntegration(t *testing.T) {
suite := setupTest(t)
sourceFile := filepath.Join("..", "..", "testdata", "integration", "class_methods.go")
sourceFile, err := filepath.Abs(sourceFile)
require.NoError(t, err)
defer suite.cleanupGeneratedFiles(sourceFile)
targetFile, err := suite.createGoModule(sourceFile)
require.NoError(t, err)
err = suite.runExtensionInit(targetFile)
require.NoError(t, err)
_, err = suite.compileFrankenPHP(filepath.Dir(targetFile))
require.NoError(t, err)
err = suite.verifyPHPSymbols(
[]string{},
[]string{"Counter", "StringHolder"},
[]string{},
)
require.NoError(t, err, "all classes should be accessible from PHP")
err = suite.verifyFunctionBehavior(`<?php
$counter = new Counter();
if ($counter->getValue() !== 0) {
echo "FAIL: Counter initial value expected 0, got " . $counter->getValue();
exit(1);
}
$counter->increment();
if ($counter->getValue() !== 1) {
echo "FAIL: Counter after increment expected 1, got " . $counter->getValue();
exit(1);
}
$counter->decrement();
if ($counter->getValue() !== 0) {
echo "FAIL: Counter after decrement expected 0, got " . $counter->getValue();
exit(1);
}
$counter->setValue(10);
if ($counter->getValue() !== 10) {
echo "FAIL: Counter after setValue(10) expected 10, got " . $counter->getValue();
exit(1);
}
$newValue = $counter->addValue(5);
if ($newValue !== 15) {
echo "FAIL: Counter addValue(5) expected to return 15, got $newValue";
exit(1);
}
if ($counter->getValue() !== 15) {
echo "FAIL: Counter value after addValue(5) expected 15, got " . $counter->getValue();
exit(1);
}
$counter->updateWithNullable(50);
if ($counter->getValue() !== 50) {
echo "FAIL: Counter after updateWithNullable(50) expected 50, got " . $counter->getValue();
exit(1);
}
$counter->updateWithNullable(null);
if ($counter->getValue() !== 50) {
echo "FAIL: Counter after updateWithNullable(null) expected 50 (unchanged), got " . $counter->getValue();
exit(1);
}
$counter->reset();
if ($counter->getValue() !== 0) {
echo "FAIL: Counter after reset expected 0, got " . $counter->getValue();
exit(1);
}
$counter1 = new Counter();
$counter2 = new Counter();
$counter1->setValue(100);
$counter2->setValue(200);
if ($counter1->getValue() !== 100 || $counter2->getValue() !== 200) {
echo "FAIL: Multiple Counter instances should be independent";
exit(1);
}
$holder = new StringHolder();
$holder->setData("test string");
if ($holder->getData() !== "test string") {
echo "FAIL: StringHolder getData expected 'test string', got '" . $holder->getData() . "'";
exit(1);
}
if ($holder->getLength() !== 11) {
echo "FAIL: StringHolder getLength expected 11, got " . $holder->getLength();
exit(1);
}
$holder->setData("");
if ($holder->getData() !== "") {
echo "FAIL: StringHolder empty string expected '', got '" . $holder->getData() . "'";
exit(1);
}
if ($holder->getLength() !== 0) {
echo "FAIL: StringHolder empty string length expected 0, got " . $holder->getLength();
exit(1);
}
echo "OK";
`, "OK")
require.NoError(t, err, "all class methods should work correctly")
}
func TestConstants(t *testing.T) {
suite := setupTest(t)
sourceFile := filepath.Join("..", "..", "testdata", "integration", "constants.go")
sourceFile, err := filepath.Abs(sourceFile)
require.NoError(t, err)
defer suite.cleanupGeneratedFiles(sourceFile)
targetFile, err := suite.createGoModule(sourceFile)
require.NoError(t, err)
err = suite.runExtensionInit(targetFile)
require.NoError(t, err)
_, err = suite.compileFrankenPHP(filepath.Dir(targetFile))
require.NoError(t, err)
err = suite.verifyPHPSymbols(
[]string{"test_with_constants"},
[]string{"Config"},
[]string{
"TEST_MAX_RETRIES", "TEST_API_VERSION", "TEST_ENABLED", "TEST_PI",
"STATUS_PENDING", "STATUS_PROCESSING", "STATUS_COMPLETED",
},
)
require.NoError(t, err, "all constants, functions, and classes should be accessible from PHP")
err = suite.verifyFunctionBehavior(`<?php
if (TEST_MAX_RETRIES !== 100) {
echo "FAIL: TEST_MAX_RETRIES expected 100, got " . TEST_MAX_RETRIES;
exit(1);
}
if (TEST_API_VERSION !== "2.0.0") {
echo "FAIL: TEST_API_VERSION expected '2.0.0', got '" . TEST_API_VERSION . "'";
exit(1);
}
if (TEST_ENABLED !== true) {
var_dump(TEST_ENABLED);
echo "FAIL: TEST_ENABLED expected true, got " . (TEST_ENABLED ? "true" : "false");
exit(1);
}
if (abs(TEST_PI - 3.14159) > 0.00001) {
echo "FAIL: TEST_PI expected 3.14159, got " . TEST_PI;
exit(1);
}
if (Config::MODE_DEBUG !== 1) {
echo "FAIL: Config::MODE_DEBUG expected 1, got " . Config::MODE_DEBUG;
exit(1);
}
if (Config::MODE_PRODUCTION !== 2) {
echo "FAIL: Config::MODE_PRODUCTION expected 2, got " . Config::MODE_PRODUCTION;
exit(1);
}
if (Config::DEFAULT_TIMEOUT !== 30) {
echo "FAIL: Config::DEFAULT_TIMEOUT expected 30, got " . Config::DEFAULT_TIMEOUT;
exit(1);
}
$config = new Config();
$config->setMode(Config::MODE_DEBUG);
if ($config->getMode() !== Config::MODE_DEBUG) {
echo "FAIL: Config getMode expected MODE_DEBUG, got " . $config->getMode();
exit(1);
}
$result = test_with_constants(STATUS_PENDING);
if ($result !== "pending") {
echo "FAIL: test_with_constants(STATUS_PENDING) expected 'pending', got '$result'";
exit(1);
}
$result = test_with_constants(STATUS_PROCESSING);
if ($result !== "processing") {
echo "FAIL: test_with_constants(STATUS_PROCESSING) expected 'processing', got '$result'";
exit(1);
}
$result = test_with_constants(STATUS_COMPLETED);
if ($result !== "completed") {
echo "FAIL: test_with_constants(STATUS_COMPLETED) expected 'completed', got '$result'";
exit(1);
}
$result = test_with_constants(999);
if ($result !== "unknown") {
echo "FAIL: test_with_constants(999) expected 'unknown', got '$result'";
exit(1);
}
echo "OK";
`, "OK")
require.NoError(t, err, "all constants should have correct values and functions should work")
}
func TestNamespace(t *testing.T) {
suite := setupTest(t)
sourceFile := filepath.Join("..", "..", "testdata", "integration", "namespace.go")
sourceFile, err := filepath.Abs(sourceFile)
require.NoError(t, err)
defer suite.cleanupGeneratedFiles(sourceFile)
targetFile, err := suite.createGoModule(sourceFile)
require.NoError(t, err)
err = suite.runExtensionInit(targetFile)
require.NoError(t, err)
_, err = suite.compileFrankenPHP(filepath.Dir(targetFile))
require.NoError(t, err)
err = suite.verifyPHPSymbols(
[]string{`\\TestIntegration\\Extension\\greet`},
[]string{`\\TestIntegration\\Extension\\Person`},
[]string{`\\TestIntegration\\Extension\\NAMESPACE_VERSION`},
)
require.NoError(t, err, "all namespaced symbols should be accessible from PHP")
err = suite.verifyFunctionBehavior(`<?php
use TestIntegration\Extension;
if (Extension\NAMESPACE_VERSION !== "1.0.0") {
echo "FAIL: NAMESPACE_VERSION expected '1.0.0', got '" . Extension\NAMESPACE_VERSION . "'";
exit(1);
}
$greeting = Extension\greet("Alice");
if ($greeting !== "Hello, Alice!") {
echo "FAIL: greet('Alice') expected 'Hello, Alice!', got '$greeting'";
exit(1);
}
$greeting = Extension\greet("");
if ($greeting !== "Hello, !") {
echo "FAIL: greet('') expected 'Hello, !', got '$greeting'";
exit(1);
}
if (Extension\Person::DEFAULT_AGE !== 18) {
echo "FAIL: Person::DEFAULT_AGE expected 18, got " . Extension\Person::DEFAULT_AGE;
exit(1);
}
$person = new Extension\Person();
$person->setName("Bob");
$person->setAge(25);
if ($person->getName() !== "Bob") {
echo "FAIL: Person getName expected 'Bob', got '" . $person->getName() . "'";
exit(1);
}
if ($person->getAge() !== 25) {
echo "FAIL: Person getAge expected 25, got " . $person->getAge();
exit(1);
}
$person->setAge(Extension\Person::DEFAULT_AGE);
if ($person->getAge() !== 18) {
echo "FAIL: Person setAge(DEFAULT_AGE) expected 18, got " . $person->getAge();
exit(1);
}
$person1 = new Extension\Person();
$person2 = new Extension\Person();
$person1->setName("Alice");
$person1->setAge(30);
$person2->setName("Charlie");
$person2->setAge(40);
if ($person1->getName() !== "Alice" || $person1->getAge() !== 30) {
echo "FAIL: person1 should have independent state";
exit(1);
}
if ($person2->getName() !== "Charlie" || $person2->getAge() !== 40) {
echo "FAIL: person2 should have independent state";
exit(1);
}
echo "OK";
`, "OK")
require.NoError(t, err, "all namespaced symbols should work correctly")
}
func TestInvalidSignature(t *testing.T) {
suite := setupTest(t)
sourceFile := filepath.Join("..", "..", "testdata", "integration", "invalid_signature.go")
sourceFile, err := filepath.Abs(sourceFile)
require.NoError(t, err)
defer suite.cleanupGeneratedFiles(sourceFile)
targetFile, err := suite.createGoModule(sourceFile)
require.NoError(t, err)
err = suite.runExtensionInit(targetFile)
assert.Error(t, err, "extension-init should fail for invalid return type")
assert.Contains(t, err.Error(), "no PHP functions, classes, or constants found", "invalid functions should be ignored, resulting in no valid exports")
}
func TestTypeMismatch(t *testing.T) {
suite := setupTest(t)
sourceFile := filepath.Join("..", "..", "testdata", "integration", "type_mismatch.go")
sourceFile, err := filepath.Abs(sourceFile)
require.NoError(t, err)
defer suite.cleanupGeneratedFiles(sourceFile)
targetFile, err := suite.createGoModule(sourceFile)
require.NoError(t, err)
err = suite.runExtensionInit(targetFile)
assert.NoError(t, err, "generation should succeed - class is valid even though function/method have type mismatches")
baseDir := filepath.Dir(targetFile)
baseName := strings.TrimSuffix(filepath.Base(targetFile), ".go")
stubFile := filepath.Join(baseDir, baseName+".stub.php")
assert.FileExists(t, stubFile, "stub file should be generated for valid class")
}
func TestMissingGenStub(t *testing.T) {
// temp override of GEN_STUB_SCRIPT
originalValue := os.Getenv("GEN_STUB_SCRIPT")
defer os.Setenv("GEN_STUB_SCRIPT", originalValue)
os.Setenv("GEN_STUB_SCRIPT", "/nonexistent/gen_stub.php")
tempDir := t.TempDir()
sourceFile := filepath.Join(tempDir, "test.go")
err := os.WriteFile(sourceFile, []byte(`package test
//export_php:function dummy(): void
func dummy() {}
`), 0o644)
require.NoError(t, err)
baseName := SanitizePackageName(strings.TrimSuffix(filepath.Base(sourceFile), ".go"))
gen := Generator{
BaseName: baseName,
SourceFile: sourceFile,
BuildDir: filepath.Dir(sourceFile),
}
err = gen.Generate()
assert.Error(t, err, "should fail when gen_stub.php is missing")
assert.Contains(t, err.Error(), "gen_stub.php", "error should mention missing script")
}
func TestCallable(t *testing.T) {
suite := setupTest(t)
sourceFile := filepath.Join("..", "..", "testdata", "integration", "callable.go")
sourceFile, err := filepath.Abs(sourceFile)
require.NoError(t, err)
defer suite.cleanupGeneratedFiles(sourceFile)
targetFile, err := suite.createGoModule(sourceFile)
require.NoError(t, err)
err = suite.runExtensionInit(targetFile)
require.NoError(t, err)
_, err = suite.compileFrankenPHP(filepath.Dir(targetFile))
require.NoError(t, err)
err = suite.verifyPHPSymbols(
[]string{"my_array_map", "my_filter"},
[]string{"Processor"},
[]string{},
)
require.NoError(t, err, "all functions and classes should be accessible from PHP")
err = suite.verifyFunctionBehavior(`<?php
$result = my_array_map([1, 2, 3], function($x) { return $x * 2; });
if ($result !== [2, 4, 6]) {
echo "FAIL: my_array_map with closure expected [2, 4, 6], got " . json_encode($result);
exit(1);
}
$result = my_array_map(['hello', 'world'], 'strtoupper');
if ($result !== ['HELLO', 'WORLD']) {
echo "FAIL: my_array_map with function name expected ['HELLO', 'WORLD'], got " . json_encode($result);
exit(1);
}
$result = my_array_map([], function($x) { return $x; });
if ($result !== []) {
echo "FAIL: my_array_map with empty array expected [], got " . json_encode($result);
exit(1);
}
$result = my_filter([1, 2, 3, 4, 5, 6], function($x) { return $x % 2 === 0; });
if ($result !== [2, 4, 6]) {
echo "FAIL: my_filter expected [2, 4, 6], got " . json_encode($result);
exit(1);
}
$result = my_filter([1, 2, 3, 4], null);
if ($result !== [1, 2, 3, 4]) {
echo "FAIL: my_filter with null callback expected [1, 2, 3, 4], got " . json_encode($result);
exit(1);
}
$processor = new Processor();
$result = $processor->transform('hello', function($s) { return strtoupper($s); });
if ($result !== 'HELLO') {
echo "FAIL: Processor::transform with closure expected 'HELLO', got '$result'";
exit(1);
}
$result = $processor->transform('world', 'strtoupper');
if ($result !== 'WORLD') {
echo "FAIL: Processor::transform with function name expected 'WORLD', got '$result'";
exit(1);
}
$result = $processor->transform(' test ', 'trim');
if ($result !== 'test') {
echo "FAIL: Processor::transform with trim expected 'test', got '$result'";
exit(1);
}
echo "OK";
`, "OK")
require.NoError(t, err, "all callable tests should pass")
}

View File

@@ -9,17 +9,18 @@ import (
type phpType string
const (
phpString phpType = "string"
phpInt phpType = "int"
phpFloat phpType = "float"
phpBool phpType = "bool"
phpArray phpType = "array"
phpObject phpType = "object"
phpMixed phpType = "mixed"
phpVoid phpType = "void"
phpNull phpType = "null"
phpTrue phpType = "true"
phpFalse phpType = "false"
phpString phpType = "string"
phpInt phpType = "int"
phpFloat phpType = "float"
phpBool phpType = "bool"
phpArray phpType = "array"
phpObject phpType = "object"
phpMixed phpType = "mixed"
phpVoid phpType = "void"
phpNull phpType = "null"
phpTrue phpType = "true"
phpFalse phpType = "false"
phpCallable phpType = "callable"
)
type phpFunction struct {

View File

@@ -68,8 +68,12 @@ func (pp *ParameterParser) generateSingleParamDeclaration(param phpParameter) []
if param.IsNullable {
decls = append(decls, fmt.Sprintf("zend_bool %s_is_null = 0;", param.Name))
}
case phpArray, phpMixed:
case phpArray:
decls = append(decls, fmt.Sprintf("zend_array *%s = NULL;", param.Name))
case phpMixed:
decls = append(decls, fmt.Sprintf("zval *%s = NULL;", param.Name))
case "callable":
decls = append(decls, fmt.Sprintf("zval *%s_callback;", param.Name))
}
return decls
@@ -118,9 +122,11 @@ func (pp *ParameterParser) generateParamParsingMacro(param phpParameter) string
case phpBool:
return fmt.Sprintf("\n Z_PARAM_BOOL_OR_NULL(%s, %s_is_null)", param.Name, param.Name)
case phpArray:
return fmt.Sprintf("\n Z_PARAM_ARRAY_OR_NULL(%s)", param.Name)
return fmt.Sprintf("\n Z_PARAM_ARRAY_HT_OR_NULL(%s)", param.Name)
case phpMixed:
return fmt.Sprintf("\n Z_PARAM_ZVAL_OR_NULL(%s)", param.Name)
case phpCallable:
return fmt.Sprintf("\n Z_PARAM_ZVAL_OR_NULL(%s_callback)", param.Name)
default:
return ""
}
@@ -135,9 +141,11 @@ func (pp *ParameterParser) generateParamParsingMacro(param phpParameter) string
case phpBool:
return fmt.Sprintf("\n Z_PARAM_BOOL(%s)", param.Name)
case phpArray:
return fmt.Sprintf("\n Z_PARAM_ARRAY(%s)", param.Name)
return fmt.Sprintf("\n Z_PARAM_ARRAY_HT(%s)", param.Name)
case phpMixed:
return fmt.Sprintf("\n Z_PARAM_ZVAL(%s)", param.Name)
case phpCallable:
return fmt.Sprintf("\n Z_PARAM_ZVAL(%s_callback)", param.Name)
default:
return ""
}
@@ -168,6 +176,8 @@ func (pp *ParameterParser) generateSingleGoCallParam(param phpParameter) string
return fmt.Sprintf("%s_is_null ? NULL : &%s", param.Name, param.Name)
case phpBool:
return fmt.Sprintf("%s_is_null ? NULL : &%s", param.Name, param.Name)
case phpCallable:
return fmt.Sprintf("%s_callback", param.Name)
default:
return param.Name
}
@@ -180,6 +190,8 @@ func (pp *ParameterParser) generateSingleGoCallParam(param phpParameter) string
return fmt.Sprintf("(double) %s", param.Name)
case phpBool:
return fmt.Sprintf("(int) %s", param.Name)
case phpCallable:
return fmt.Sprintf("%s_callback", param.Name)
default:
return param.Name
}

View File

@@ -145,14 +145,14 @@ func TestParameterParser_GenerateParamDeclarations(t *testing.T) {
params: []phpParameter{
{Name: "items", PhpType: phpArray, HasDefault: false},
},
expected: " zval *items = NULL;",
expected: " zend_array *items = NULL;",
},
{
name: "nullable array parameter",
params: []phpParameter{
{Name: "items", PhpType: phpArray, HasDefault: false, IsNullable: true},
},
expected: " zval *items = NULL;",
expected: " zend_array *items = NULL;",
},
{
name: "mixed types with array",
@@ -161,7 +161,7 @@ func TestParameterParser_GenerateParamDeclarations(t *testing.T) {
{Name: "items", PhpType: phpArray, HasDefault: false},
{Name: "count", PhpType: phpInt, HasDefault: true, DefaultValue: "5"},
},
expected: " zend_string *name = NULL;\n zval *items = NULL;\n zend_long count = 5;",
expected: " zend_string *name = NULL;\n zend_array *items = NULL;\n zend_long count = 5;",
},
{
name: "mixed parameter",
@@ -177,6 +177,29 @@ func TestParameterParser_GenerateParamDeclarations(t *testing.T) {
},
expected: " zval *m = NULL;",
},
{
name: "callable parameter",
params: []phpParameter{
{Name: "callback", PhpType: phpCallable, HasDefault: false},
},
expected: " zval *callback_callback;",
},
{
name: "nullable callable parameter",
params: []phpParameter{
{Name: "callback", PhpType: phpCallable, HasDefault: false, IsNullable: true},
},
expected: " zval *callback_callback;",
},
{
name: "mixed types with callable",
params: []phpParameter{
{Name: "data", PhpType: phpArray, HasDefault: false},
{Name: "callback", PhpType: phpCallable, HasDefault: false},
{Name: "options", PhpType: phpInt, HasDefault: true, DefaultValue: "0"},
},
expected: " zend_array *data = NULL;\n zval *callback_callback;\n zend_long options = 0;",
},
}
for _, tt := range tests {
@@ -292,6 +315,29 @@ func TestParameterParser_GenerateGoCallParams(t *testing.T) {
},
expected: "name, items, (long) count",
},
{
name: "callable parameter",
params: []phpParameter{
{Name: "callback", PhpType: "callable"},
},
expected: "callback_callback",
},
{
name: "nullable callable parameter",
params: []phpParameter{
{Name: "callback", PhpType: "callable", IsNullable: true},
},
expected: "callback_callback",
},
{
name: "mixed parameters with callable",
params: []phpParameter{
{Name: "data", PhpType: "array"},
{Name: "callback", PhpType: "callable"},
{Name: "limit", PhpType: "int"},
},
expected: "data, callback_callback, (long) limit",
},
}
for _, tt := range tests {
@@ -353,12 +399,12 @@ func TestParameterParser_GenerateParamParsingMacro(t *testing.T) {
{
name: "array parameter",
param: phpParameter{Name: "items", PhpType: phpArray},
expected: "\n Z_PARAM_ARRAY(items)",
expected: "\n Z_PARAM_ARRAY_HT(items)",
},
{
name: "nullable array parameter",
param: phpParameter{Name: "items", PhpType: phpArray, IsNullable: true},
expected: "\n Z_PARAM_ARRAY_OR_NULL(items)",
expected: "\n Z_PARAM_ARRAY_HT_OR_NULL(items)",
},
{
name: "mixed parameter",
@@ -370,6 +416,16 @@ func TestParameterParser_GenerateParamParsingMacro(t *testing.T) {
param: phpParameter{Name: "m", PhpType: phpMixed, IsNullable: true},
expected: "\n Z_PARAM_ZVAL_OR_NULL(m)",
},
{
name: "callable parameter",
param: phpParameter{Name: "callback", PhpType: phpCallable},
expected: "\n Z_PARAM_ZVAL(callback_callback)",
},
{
name: "nullable callable parameter",
param: phpParameter{Name: "callback", PhpType: phpCallable, IsNullable: true},
expected: "\n Z_PARAM_ZVAL_OR_NULL(callback_callback)",
},
{
name: "unknown type",
param: phpParameter{Name: "unknown", PhpType: phpType("unknown")},
@@ -480,6 +536,16 @@ func TestParameterParser_GenerateSingleGoCallParam(t *testing.T) {
param: phpParameter{Name: "items", PhpType: phpArray, IsNullable: true},
expected: "items",
},
{
name: "callable parameter",
param: phpParameter{Name: "callback", PhpType: "callable"},
expected: "callback_callback",
},
{
name: "nullable callable parameter",
param: phpParameter{Name: "callback", PhpType: "callable", IsNullable: true},
expected: "callback_callback",
},
{
name: "unknown type",
param: phpParameter{Name: "unknown", PhpType: phpType("unknown")},
@@ -551,12 +617,22 @@ func TestParameterParser_GenerateSingleParamDeclaration(t *testing.T) {
{
name: "array parameter",
param: phpParameter{Name: "items", PhpType: phpArray, HasDefault: false},
expected: []string{"zval *items = NULL;"},
expected: []string{"zend_array *items = NULL;"},
},
{
name: "nullable array parameter",
param: phpParameter{Name: "items", PhpType: phpArray, HasDefault: false, IsNullable: true},
expected: []string{"zval *items = NULL;"},
expected: []string{"zend_array *items = NULL;"},
},
{
name: "callable parameter",
param: phpParameter{Name: "callback", PhpType: "callable", HasDefault: false},
expected: []string{"zval *callback_callback;"},
},
{
name: "nullable callable parameter",
param: phpParameter{Name: "callback", PhpType: "callable", HasDefault: false, IsNullable: true},
expected: []string{"zval *callback_callback;"},
},
}

Some files were not shown because too many files have changed in this diff Show More