Repository: cilium/ebpf
Pull request #1650 (2025-01-21 10:19:43)

add windows CI
This is a precursor to further Windows work, mostly stubbing out things that won't ever work on Windows, or for which there isn't a plan yet.

examples: add linux build tag

    All of our examples will only work on Linux, some of them will only compile
    on Unix-like systems due to depending on x/sys/unix. Add a linux build tag
    to them.

    Signed-off-by: Lorenz Bauer <lmb@isovalent.com>

features: add linux build tag

    Signed-off-by: Lorenz Bauer <lmb@isovalent.com>

perf, ringbuf, epoll: add linux build tag

    Signed-off-by: Lorenz Bauer <lmb@isovalent.com>

cmd/bpf2go: disable on Windows

    Signed-off-by: Lorenz Bauer <lmb@isovalent.com>

unix: return ErrNotSupportedOnOS for non-Linux platforms

    Change the dummy unix package to return a sentinel error on non-Linux 
    platforms. This means that we don't need to add lots of runtime.GOOS checks
    further up the call stack.

    Signed-off-by: Lorenz Bauer <lmb@isovalent.com>

internal/tracefs: skip some tests on non-Linux platforms

    Signed-off-by: Lorenz Bauer <lmb@isovalent.com>

internal/kallsyms: skip some tests on non-Linux platforms

    Signed-off-by: Lorenz Bauer <lmb@isovalent.com>

internal/linux: skip some tests on non-Linux platforms

    Skip most tests in the package, with the exception of the vdso tests that
    use testdata. For those we need to make the string manipulation functions in
    package unix work on Windows.

    Signed-off-by: Lorenz Bauer <lmb@isovalent.com>

CI: add windows job

    Add a windows job wich installs a debug version of eBPF for Windows, the go
    toolchain and gotestsum.

    For some reason, drive C: is exceedingly slow on the hosted Windows runners.
    Moving cache and temporary directories cuts down the build time of gotestsum
    from 2 minutes to 20ish seconds.

    For now we only test packages which already pass.

    Signed-off-by: Lorenz Bauer <lmb@isovalent.com>

CI: use latest main build of efW

    Use the latest successful build of the main branch of eBPF for Windows. This
    makes more sense than using tagged versions since most of the changes we
    need are yet to be upstreamed.

    Staying on a tagged release would mean waiting six weeks for a new release,
    on average.

    Signed-off-by: Lorenz Bauer <lmb@isovalent.com>

internal: add OnLinux constant

    Add a constant which indicates whether we are building for Linux. This is so
    that we can write

    	if !internal.OnLinux {

    instead of

    	if runtime.GOOS != "linux" {

    This makes it easy to remove platform dependent code by removing the 
    constant and then fixing the breakage.

    Signed-off-by: Lorenz Bauer <lmb@isovalent.com>


lmb: > Having led the effort of removing build tags from cilium/cilium, I know what a pain they can be and how they quickly lead to a slippery slope.

I think you're referring to the integration_tests build tag? In my opinion there is a difference between that and what I am doing here: the GOOS build tag is always set by the toolchain. So whether you're developing on Windows or on Linux, you always get exactly one configuration. In cilium/cilium this wasn't the case.

To be honest I don't think it's possible to develop cross-platform without using build tags in some fashion.

> having a mix of build tags and individual test skips will lead to silently skipping tests you're expecting to run on Windows

You're worried that we'll end up disabling too many tests on Windows? Probably a fair point, I can do another pass over the stubbed out packages and see whether they do make sense after all. Not sure we can do better than "pay attention at the outset".

> [...] reason about a better solution at the moment, but it's an exercise we should do before committing to this approach

Maybe the problem is that this PR only contains half of my approach:

1. Packages that wont exist on Windows (for now) or tests that will never work on the platform are skipped based on runtime.GOOS (if there are other tests that do make sense to run) or build tags (to avoid adding a million runtime.GOOS conditions).
2. Packages that will exist on Windows but which are not fully implemented, for example package btf. We need the btf reader but loading is not supported. The way this will work is that the new ErrNotSupportedOnOS sentinel is returned on Windows, and tests are updated to call [testutils.SkipIfNotSupportedOnOS](https://github.com/cilium/ebpf/blob/278a914142727d30c5f5af26745a6aa51d085bd9/internal/testutils/feature.go#L61-L71).

I've only included part 1 here, maybe that's why this seems a bit ad hoc.
lmb: Okay, I made two bigger changes:

1. Made functions in `internal/unix` return ErrNotSupportedOnOS by default. This will save adding a lot of `if runtime.GOOS` checks later on. See https://github.com/cilium/ebpf/pull/1650/commits/d9aada082e325817beef3a27e3b6e668554324b5
2. Removed all build tags from tests and added SkipIfNotSupported instead. This adds one line to most tests but has the benefit that tests start executing once a particular "not supportedness" is fixed. That in turn will help in  making the API consistent.

I also added the `OnLinux` constant as the top commit, so I can remove it easily if it's not worth it.

Patch:
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index be1535e..01ac4cd 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -8,6 +8,7 @@ on:
 env:
   TMPDIR: /tmp
   CI_MAX_KERNEL_VERSION: '6.11'
+  CI_MAX_EFW_VERSION: '0.20.0'
   CI_MIN_CLANG_VERSION: '11'
   go_version: '~1.23'
   prev_go_version: '~1.22'
@@ -185,8 +186,8 @@ jobs:
         run: |
           sudo dmesg
 
-  vm-test:
-    name: Run tests
+  linux-test:
+    name: Run tests (Linux)
     runs-on: ubuntu-latest
     needs: build-and-lint
     timeout-minutes: 15
@@ -223,5 +224,139 @@ jobs:
         if: always()
         uses: actions/upload-artifact@v4
         with:
-          name: Test Results (${{ matrix.tag }})
+          name: Test Results (linux ${{ matrix.tag }})
           path: junit.xml
+
+  windows-test:
+    name: Run tests (Windows)
+    runs-on: windows-2022
+    needs: build-and-lint
+    timeout-minutes: 15
+    strategy:
+      matrix:
+        version:
+          - "main"
+    env:
+      # Fix slow Go compile and cache restore
+      # See https://github.com/actions/setup-go/pull/515
+      GOCACHE: D:\gocache
+      GOMODCACHE: D:\gomodcache
+      # Avoid putting temp on slow C:
+      TEMP: D:\temp
+
+    steps:
+      - run: mkdir D:\temp
+        shell: pwsh
+
+      - name: Get eBPF for Windows download URL
+        id: determine-url
+        uses: actions/github-script@v6
+        with:
+          script: |
+            if ("${{ matrix.version }}" != "main") {
+              // TODO: Workflow artifact and release artifact don't have the
+              // same folder structure.
+              core.setFailed('Installing tagged versions is not supported');
+              return;
+            }
+
+            // Get the latest successful merge_group run
+            const workflow_runs = await github.rest.actions.listWorkflowRuns({
+              owner: 'microsoft',
+              repo: 'ebpf-for-windows',
+              workflow_id: 'cicd.yml',
+              event: 'merge_group',
+              status: 'success',
+              per_page: 1
+            });
+
+            if (workflow_runs.data.workflow_runs.length === 0) {
+              core.setFailed('No successful merge_group workflow runs found');
+              return;
+            }
+
+            // Get artifacts from this run
+            const run_id = workflow_runs.data.workflow_runs[0].id;
+            const artifacts = await github.rest.actions.listWorkflowRunArtifacts({
+              owner: 'microsoft',
+              repo: 'ebpf-for-windows',
+              run_id: run_id
+            });
+
+            // Find the specific artifact
+            const artifact = artifacts.data.artifacts.find(a => a.name === 'Build-x64-Debug');
+
+            if (!artifact) {
+              console.log('Available artifacts:', artifacts.data.artifacts.map(a => a.name));
+              core.setFailed('Build-x64-Debug artifact not found in the workflow run');
+              return;
+            }
+
+            // Get the download URL via redirect
+            const response = await github.rest.actions.downloadArtifact({
+              owner: 'microsoft',
+              repo: 'ebpf-for-windows',
+              artifact_id: artifact.id,
+              archive_format: 'zip',
+              request: {
+                redirect: 'manual'
+              }
+            });
+
+            // Extract the location header which contains the actual download URL
+            const download_url = response.headers.location;
+
+            if (!download_url) {
+              core.setFailed('Failed to get redirect URL from headers');
+              return;
+            }
+
+            core.setOutput('download_url', download_url);
+
+      - name: Download and Install eBPF for Windows
+        shell: pwsh
+        run: |
+          Invoke-WebRequest -Uri "${{ steps.determine-url.outputs.download_url }}" -OutFile "$env:TEMP\efw.zip"
+          Expand-Archive -Path "$env:TEMP\efw.zip" -DestinationPath "$env:TEMP"
+          Expand-Archive -Path "$env:TEMP\build-Debug.zip" -DestinationPath "$env:TEMP\ebpf"
+          $setupScript = Get-ChildItem -Path "$env:TEMP\ebpf" -Filter "setup-ebpf.ps1" -Recurse | Select-Object -First 1
+          if ($setupScript) {
+            Write-Host "Found setup script: $($setupScript.FullName)"
+            Set-Location -Path $setupScript.DirectoryName
+            Write-Host "Changed directory to: $(Get-Location)"
+            & $setupScript.FullName
+          } else {
+            Write-Error "Setup script not found in the extracted package"
+            exit 1
+          }
+
+      - uses: actions/checkout@v4
+
+      - name: Set up Go
+        uses: actions/setup-go@v5
+        with:
+          go-version: '${{ env.go_version }}'
+
+      - run: go install gotest.tools/gotestsum@v1.8.1
+
+      - name: Test
+        run: >
+          gotestsum --raw-command --ignore-non-json-output-lines --junitfile junit.xml --
+          go test -short -count 1 -json
+          ./asm
+          ./internal
+          ./internal/kallsyms
+          ./internal/kconfig
+          ./internal/linux
+          ./internal/sysenc
+          ./internal/testutils
+          ./internal/tracefs
+          ./internal/unix
+
+      - name: Upload Test Results
+        if: always()
+        uses: actions/upload-artifact@v4
+        with:
+          name: Test Results (windows ${{ matrix.tag }})
+          path: junit.xml
+
diff --git a/cmd/bpf2go/doc.go b/cmd/bpf2go/doc.go
index 138eb4e..99798ff 100644
--- a/cmd/bpf2go/doc.go
+++ b/cmd/bpf2go/doc.go
@@ -1,3 +1,5 @@
+//go:build !windows
+
 // Program bpf2go embeds eBPF in Go.
 //
 // Please see the README for details how to use it.
diff --git a/cmd/bpf2go/flags.go b/cmd/bpf2go/flags.go
index 806d2cd..c1f70c7 100644
--- a/cmd/bpf2go/flags.go
+++ b/cmd/bpf2go/flags.go
@@ -1,3 +1,5 @@
+//go:build !windows
+
 package main
 
 import (
diff --git a/cmd/bpf2go/gen/compile.go b/cmd/bpf2go/gen/compile.go
index 09d57da..efd05fb 100644
--- a/cmd/bpf2go/gen/compile.go
+++ b/cmd/bpf2go/gen/compile.go
@@ -1,3 +1,5 @@
+//go:build !windows
+
 package gen
 
 import (
diff --git a/cmd/bpf2go/gen/output.go b/cmd/bpf2go/gen/output.go
index b8855c7..9f0fc06 100644
--- a/cmd/bpf2go/gen/output.go
+++ b/cmd/bpf2go/gen/output.go
@@ -1,3 +1,5 @@
+//go:build !windows
+
 package gen
 
 import (
diff --git a/cmd/bpf2go/gen/target.go b/cmd/bpf2go/gen/target.go
index f5484cc..1e0247c 100644
--- a/cmd/bpf2go/gen/target.go
+++ b/cmd/bpf2go/gen/target.go
@@ -1,3 +1,5 @@
+//go:build !windows
+
 package gen
 
 import (
diff --git a/cmd/bpf2go/gen/types.go b/cmd/bpf2go/gen/types.go
index 115d734..f1413c1 100644
--- a/cmd/bpf2go/gen/types.go
+++ b/cmd/bpf2go/gen/types.go
@@ -1,3 +1,5 @@
+//go:build !windows
+
 package gen
 
 import (
diff --git a/cmd/bpf2go/main.go b/cmd/bpf2go/main.go
index 91e51e3..9408be1 100644
--- a/cmd/bpf2go/main.go
+++ b/cmd/bpf2go/main.go
@@ -1,3 +1,5 @@
+//go:build !windows
+
 package main
 
 import (
diff --git a/cmd/bpf2go/makedep.go b/cmd/bpf2go/makedep.go
index 9f4973c..b5d34b7 100644
--- a/cmd/bpf2go/makedep.go
+++ b/cmd/bpf2go/makedep.go
@@ -1,3 +1,5 @@
+//go:build !windows
+
 package main
 
 import (
diff --git a/cmd/bpf2go/tools.go b/cmd/bpf2go/tools.go
index e754d7a..331274b 100644
--- a/cmd/bpf2go/tools.go
+++ b/cmd/bpf2go/tools.go
@@ -1,3 +1,5 @@
+//go:build !windows
+
 package main
 
 import (
diff --git a/docs/examples/getting_started/counter_bpfeb.go b/docs/examples/getting_started/counter_bpfeb.go
index 2ebc5eb..7fed929 100644
--- a/docs/examples/getting_started/counter_bpfeb.go
+++ b/docs/examples/getting_started/counter_bpfeb.go
@@ -1,5 +1,5 @@
 // Code generated by bpf2go; DO NOT EDIT.
-//go:build mips || mips64 || ppc64 || s390x
+//go:build (mips || mips64 || ppc64 || s390x) && linux
 
 package main
 
diff --git a/docs/examples/getting_started/counter_bpfel.go b/docs/examples/getting_started/counter_bpfel.go
index ca57ac8..2855422 100644
--- a/docs/examples/getting_started/counter_bpfel.go
+++ b/docs/examples/getting_started/counter_bpfel.go
@@ -1,5 +1,5 @@
 // Code generated by bpf2go; DO NOT EDIT.
-//go:build 386 || amd64 || arm || arm64 || loong64 || mips64le || mipsle || ppc64le || riscv64
+//go:build (386 || amd64 || arm || arm64 || loong64 || mips64le || mipsle || ppc64le || riscv64) && linux
 
 package main
 
diff --git a/docs/examples/getting_started/gen.go b/docs/examples/getting_started/gen.go
index 28bccd4..9135e18 100644
--- a/docs/examples/getting_started/gen.go
+++ b/docs/examples/getting_started/gen.go
@@ -1,6 +1,8 @@
+//go:build linux
+
 // getting_started_gen {
 package main
 
-//go:generate go run github.com/cilium/ebpf/cmd/bpf2go counter counter.c
+//go:generate go run github.com/cilium/ebpf/cmd/bpf2go -tags linux counter counter.c
 
 // }
diff --git a/docs/examples/getting_started/main.go b/docs/examples/getting_started/main.go
index 7a29aa1..f2cb915 100644
--- a/docs/examples/getting_started/main.go
+++ b/docs/examples/getting_started/main.go
@@ -1,3 +1,5 @@
+//go:build linux
+
 // getting_started_main {
 package main
 
diff --git a/examples/cgroup_skb/bpf_bpfeb.go b/examples/cgroup_skb/bpf_bpfeb.go
index eaa7d67..d2b4b6f 100644
--- a/examples/cgroup_skb/bpf_bpfeb.go
+++ b/examples/cgroup_skb/bpf_bpfeb.go
@@ -1,5 +1,5 @@
 // Code generated by bpf2go; DO NOT EDIT.
-//go:build mips || mips64 || ppc64 || s390x
+//go:build (mips || mips64 || ppc64 || s390x) && linux
 
 package main
 
diff --git a/examples/cgroup_skb/bpf_bpfel.go b/examples/cgroup_skb/bpf_bpfel.go
index 67f5ba4..dc89696 100644
--- a/examples/cgroup_skb/bpf_bpfel.go
+++ b/examples/cgroup_skb/bpf_bpfel.go
@@ -1,5 +1,5 @@
 // Code generated by bpf2go; DO NOT EDIT.
-//go:build 386 || amd64 || arm || arm64 || loong64 || mips64le || mipsle || ppc64le || riscv64
+//go:build (386 || amd64 || arm || arm64 || loong64 || mips64le || mipsle || ppc64le || riscv64) && linux
 
 package main
 
diff --git a/examples/cgroup_skb/main.go b/examples/cgroup_skb/main.go
index 7738001..c6061b3 100644
--- a/examples/cgroup_skb/main.go
+++ b/examples/cgroup_skb/main.go
@@ -1,3 +1,5 @@
+//go:build linux
+
 // This program demonstrates attaching an eBPF program to a control group.
 // The eBPF program will be attached as an egress filter,
 // receiving an `__sk_buff` pointer for each outgoing packet.
@@ -17,7 +19,7 @@ import (
 	"github.com/cilium/ebpf/rlimit"
 )
 
-//go:generate go run github.com/cilium/ebpf/cmd/bpf2go bpf cgroup_skb.c -- -I../headers
+//go:generate go run github.com/cilium/ebpf/cmd/bpf2go -tags linux bpf cgroup_skb.c -- -I../headers
 
 func main() {
 	// Allow the current process to lock memory for eBPF resources.
diff --git a/examples/fentry/bpf_bpfeb.go b/examples/fentry/bpf_bpfeb.go
index fd2aeb7..7fab2f4 100644
--- a/examples/fentry/bpf_bpfeb.go
+++ b/examples/fentry/bpf_bpfeb.go
@@ -1,5 +1,5 @@
 // Code generated by bpf2go; DO NOT EDIT.
-//go:build mips || mips64 || ppc64 || s390x
+//go:build (mips || mips64 || ppc64 || s390x) && linux
 
 package main
 
diff --git a/examples/fentry/bpf_bpfel.go b/examples/fentry/bpf_bpfel.go
index 1b4d2a2..8cd2a09 100644
--- a/examples/fentry/bpf_bpfel.go
+++ b/examples/fentry/bpf_bpfel.go
@@ -1,5 +1,5 @@
 // Code generated by bpf2go; DO NOT EDIT.
-//go:build 386 || amd64 || arm || arm64 || loong64 || mips64le || mipsle || ppc64le || riscv64
+//go:build (386 || amd64 || arm || arm64 || loong64 || mips64le || mipsle || ppc64le || riscv64) && linux
 
 package main
 
diff --git a/examples/fentry/main.go b/examples/fentry/main.go
index dcf5442..3d5670c 100644
--- a/examples/fentry/main.go
+++ b/examples/fentry/main.go
@@ -1,3 +1,5 @@
+//go:build linux
+
 // This program demonstrates attaching a fentry eBPF program to
 // tcp_connect. It prints the command/IPs/ports information
 // once the host sent a TCP SYN packet to a destination.
@@ -10,7 +12,6 @@
 // 2021/11/06 17:51:25 wget   10.0.2.15     49850  -> 142.250.72.228   443
 // 2021/11/06 17:51:46 ssh    10.0.2.15     58854  -> 10.0.2.1         22
 // 2021/11/06 18:13:15 curl   10.0.2.15     54268  -> 104.21.1.217     80
-
 package main
 
 import (
@@ -28,7 +29,7 @@ import (
 	"github.com/cilium/ebpf/rlimit"
 )
 
-//go:generate go run github.com/cilium/ebpf/cmd/bpf2go -type event bpf fentry.c -- -I../headers
+//go:generate go run github.com/cilium/ebpf/cmd/bpf2go -tags linux -type event bpf fentry.c -- -I../headers
 
 func main() {
 	stopper := make(chan os.Signal, 1)
diff --git a/examples/kprobe/bpf_bpfeb.go b/examples/kprobe/bpf_bpfeb.go
index c75e686..f2c6ed8 100644
--- a/examples/kprobe/bpf_bpfeb.go
+++ b/examples/kprobe/bpf_bpfeb.go
@@ -1,5 +1,5 @@
 // Code generated by bpf2go; DO NOT EDIT.
-//go:build mips || mips64 || ppc64 || s390x
+//go:build (mips || mips64 || ppc64 || s390x) && linux
 
 package main
 
diff --git a/examples/kprobe/bpf_bpfel.go b/examples/kprobe/bpf_bpfel.go
index d252db9..b82cd79 100644
--- a/examples/kprobe/bpf_bpfel.go
+++ b/examples/kprobe/bpf_bpfel.go
@@ -1,5 +1,5 @@
 // Code generated by bpf2go; DO NOT EDIT.
-//go:build 386 || amd64 || arm || arm64 || loong64 || mips64le || mipsle || ppc64le || riscv64
+//go:build (386 || amd64 || arm || arm64 || loong64 || mips64le || mipsle || ppc64le || riscv64) && linux
 
 package main
 
diff --git a/examples/kprobe/main.go b/examples/kprobe/main.go
index cfc9601..4b6fb3e 100644
--- a/examples/kprobe/main.go
+++ b/examples/kprobe/main.go
@@ -1,3 +1,5 @@
+//go:build linux
+
 // This program demonstrates attaching an eBPF program to a kernel symbol.
 // The eBPF program will be attached to the start of the sys_execve
 // kernel function and prints out the number of times it has been called
@@ -12,7 +14,7 @@ import (
 	"github.com/cilium/ebpf/rlimit"
 )
 
-//go:generate go run github.com/cilium/ebpf/cmd/bpf2go bpf kprobe.c -- -I../headers
+//go:generate go run github.com/cilium/ebpf/cmd/bpf2go -tags linux bpf kprobe.c -- -I../headers
 
 const mapKey uint32 = 0
 
diff --git a/examples/kprobe_percpu/bpf_bpfeb.go b/examples/kprobe_percpu/bpf_bpfeb.go
index c75e686..f2c6ed8 100644
--- a/examples/kprobe_percpu/bpf_bpfeb.go
+++ b/examples/kprobe_percpu/bpf_bpfeb.go
@@ -1,5 +1,5 @@
 // Code generated by bpf2go; DO NOT EDIT.
-//go:build mips || mips64 || ppc64 || s390x
+//go:build (mips || mips64 || ppc64 || s390x) && linux
 
 package main
 
diff --git a/examples/kprobe_percpu/bpf_bpfel.go b/examples/kprobe_percpu/bpf_bpfel.go
index d252db9..b82cd79 100644
--- a/examples/kprobe_percpu/bpf_bpfel.go
+++ b/examples/kprobe_percpu/bpf_bpfel.go
@@ -1,5 +1,5 @@
 // Code generated by bpf2go; DO NOT EDIT.
-//go:build 386 || amd64 || arm || arm64 || loong64 || mips64le || mipsle || ppc64le || riscv64
+//go:build (386 || amd64 || arm || arm64 || loong64 || mips64le || mipsle || ppc64le || riscv64) && linux
 
 package main
 
diff --git a/examples/kprobe_percpu/main.go b/examples/kprobe_percpu/main.go
index c412384..7245066 100644
--- a/examples/kprobe_percpu/main.go
+++ b/examples/kprobe_percpu/main.go
@@ -1,3 +1,5 @@
+//go:build linux
+
 // This program demonstrates attaching an eBPF program to a kernel symbol and
 // using percpu map to collect data. The eBPF program will be attached to the
 // start of the sys_execve kernel function and prints out the number of called
@@ -12,7 +14,7 @@ import (
 	"github.com/cilium/ebpf/rlimit"
 )
 
-//go:generate go run github.com/cilium/ebpf/cmd/bpf2go bpf kprobe_percpu.c -- -I../headers
+//go:generate go run github.com/cilium/ebpf/cmd/bpf2go -tags linux bpf kprobe_percpu.c -- -I../headers
 
 const mapKey uint32 = 0
 
diff --git a/examples/kprobepin/bpf_bpfeb.go b/examples/kprobepin/bpf_bpfeb.go
index c75e686..f2c6ed8 100644
--- a/examples/kprobepin/bpf_bpfeb.go
+++ b/examples/kprobepin/bpf_bpfeb.go
@@ -1,5 +1,5 @@
 // Code generated by bpf2go; DO NOT EDIT.
-//go:build mips || mips64 || ppc64 || s390x
+//go:build (mips || mips64 || ppc64 || s390x) && linux
 
 package main
 
diff --git a/examples/kprobepin/bpf_bpfel.go b/examples/kprobepin/bpf_bpfel.go
index d252db9..b82cd79 100644
--- a/examples/kprobepin/bpf_bpfel.go
+++ b/examples/kprobepin/bpf_bpfel.go
@@ -1,5 +1,5 @@
 // Code generated by bpf2go; DO NOT EDIT.
-//go:build 386 || amd64 || arm || arm64 || loong64 || mips64le || mipsle || ppc64le || riscv64
+//go:build (386 || amd64 || arm || arm64 || loong64 || mips64le || mipsle || ppc64le || riscv64) && linux
 
 package main
 
diff --git a/examples/kprobepin/main.go b/examples/kprobepin/main.go
index 4eecd4b..46cb0ac 100644
--- a/examples/kprobepin/main.go
+++ b/examples/kprobepin/main.go
@@ -1,3 +1,5 @@
+//go:build linux
+
 // This program demonstrates attaching an eBPF program to a kernel symbol.
 // The eBPF program will be attached to the start of the sys_execve
 // kernel function and prints out the number of times it has been called
@@ -15,7 +17,7 @@ import (
 	"github.com/cilium/ebpf/rlimit"
 )
 
-//go:generate go run github.com/cilium/ebpf/cmd/bpf2go bpf kprobe_pin.c -- -I../headers
+//go:generate go run github.com/cilium/ebpf/cmd/bpf2go -tags linux bpf kprobe_pin.c -- -I../headers
 
 const (
 	mapKey    uint32 = 0
diff --git a/examples/map_in_map/main.go b/examples/map_in_map/main.go
index 243ebe2..70446f5 100644
--- a/examples/map_in_map/main.go
+++ b/examples/map_in_map/main.go
@@ -1,3 +1,5 @@
+//go:build linux
+
 // An example of using maps within maps. This example demonstrates a few
 // features. Firstly, creating eBPF map specifications in pure Go
 // (typically you'd see them being generated from a loaded ELF).
diff --git a/examples/tcprtt/bpf_bpfeb.go b/examples/tcprtt/bpf_bpfeb.go
index 9567721..8d320fd 100644
--- a/examples/tcprtt/bpf_bpfeb.go
+++ b/examples/tcprtt/bpf_bpfeb.go
@@ -1,5 +1,5 @@
 // Code generated by bpf2go; DO NOT EDIT.
-//go:build mips || mips64 || ppc64 || s390x
+//go:build (mips || mips64 || ppc64 || s390x) && linux
 
 package main
 
diff --git a/examples/tcprtt/bpf_bpfel.go b/examples/tcprtt/bpf_bpfel.go
index 4eb5dcd..e40abf6 100644
--- a/examples/tcprtt/bpf_bpfel.go
+++ b/examples/tcprtt/bpf_bpfel.go
@@ -1,5 +1,5 @@
 // Code generated by bpf2go; DO NOT EDIT.
-//go:build 386 || amd64 || arm || arm64 || loong64 || mips64le || mipsle || ppc64le || riscv64
+//go:build (386 || amd64 || arm || arm64 || loong64 || mips64le || mipsle || ppc64le || riscv64) && linux
 
 package main
 
diff --git a/examples/tcprtt/main.go b/examples/tcprtt/main.go
index 7a22969..926400f 100644
--- a/examples/tcprtt/main.go
+++ b/examples/tcprtt/main.go
@@ -1,3 +1,5 @@
+//go:build linux
+
 // This program demonstrates attaching a fentry eBPF program to
 // tcp_close and reading the RTT from the TCP socket using CO-RE helpers.
 // It prints the IPs/ports/RTT information
@@ -11,7 +13,6 @@
 // 2022/03/19 22:30:36 10.0.1.205      50578  -> 117.102.109.186 5201   195
 // 2022/03/19 22:30:53 10.0.1.205      0      -> 89.84.1.178     9200   30
 // 2022/03/19 22:30:53 10.0.1.205      36022  -> 89.84.1.178     9200   28
-
 package main
 
 import (
@@ -29,7 +30,7 @@ import (
 	"github.com/cilium/ebpf/rlimit"
 )
 
-//go:generate go run github.com/cilium/ebpf/cmd/bpf2go -type event bpf tcprtt.c -- -I../headers
+//go:generate go run github.com/cilium/ebpf/cmd/bpf2go -tags linux -type event bpf tcprtt.c -- -I../headers
 
 func main() {
 	stopper := make(chan os.Signal, 1)
diff --git a/examples/tcprtt_sockops/main.go b/examples/tcprtt_sockops/main.go
index 15a224d..56bf5c2 100644
--- a/examples/tcprtt_sockops/main.go
+++ b/examples/tcprtt_sockops/main.go
@@ -39,7 +39,7 @@ import (
 	"golang.org/x/sys/unix"
 )
 
-//go:generate go run github.com/cilium/ebpf/cmd/bpf2go -tags "linux" -type rtt_event bpf tcprtt_sockops.c -- -I../headers
+//go:generate go run github.com/cilium/ebpf/cmd/bpf2go -tags linux -tags "linux" -type rtt_event bpf tcprtt_sockops.c -- -I../headers
 
 func main() {
 	stopper := make(chan os.Signal, 1)
diff --git a/examples/tcx/bpf_bpfeb.go b/examples/tcx/bpf_bpfeb.go
index 3883b86..45f4045 100644
--- a/examples/tcx/bpf_bpfeb.go
+++ b/examples/tcx/bpf_bpfeb.go
@@ -1,5 +1,5 @@
 // Code generated by bpf2go; DO NOT EDIT.
-//go:build mips || mips64 || ppc64 || s390x
+//go:build (mips || mips64 || ppc64 || s390x) && linux
 
 package main
 
diff --git a/examples/tcx/bpf_bpfel.go b/examples/tcx/bpf_bpfel.go
index 56d03d1..d23feed 100644
--- a/examples/tcx/bpf_bpfel.go
+++ b/examples/tcx/bpf_bpfel.go
@@ -1,5 +1,5 @@
 // Code generated by bpf2go; DO NOT EDIT.
-//go:build 386 || amd64 || arm || arm64 || loong64 || mips64le || mipsle || ppc64le || riscv64
+//go:build (386 || amd64 || arm || arm64 || loong64 || mips64le || mipsle || ppc64le || riscv64) && linux
 
 package main
 
diff --git a/examples/tcx/main.go b/examples/tcx/main.go
index d31acb1..12aa459 100644
--- a/examples/tcx/main.go
+++ b/examples/tcx/main.go
@@ -1,3 +1,5 @@
+//go:build linux
+
 // This program demonstrates attaching an eBPF program to a network interface
 // with Linux TCX (Traffic Control with eBPF). The program counts ingress and egress
 // packets using two variables. The userspace program (Go code in this file)
@@ -16,7 +18,7 @@ import (
 	"github.com/cilium/ebpf/link"
 )
 
-//go:generate go run github.com/cilium/ebpf/cmd/bpf2go bpf tcx.c -- -I../headers
+//go:generate go run github.com/cilium/ebpf/cmd/bpf2go -tags linux bpf tcx.c -- -I../headers
 func main() {
 	if len(os.Args) < 2 {
 		log.Fatalf("Please specify a network interface")
diff --git a/examples/tracepoint_in_c/bpf_bpfeb.go b/examples/tracepoint_in_c/bpf_bpfeb.go
index a2afd6f..2129bc7 100644
--- a/examples/tracepoint_in_c/bpf_bpfeb.go
+++ b/examples/tracepoint_in_c/bpf_bpfeb.go
@@ -1,5 +1,5 @@
 // Code generated by bpf2go; DO NOT EDIT.
-//go:build mips || mips64 || ppc64 || s390x
+//go:build (mips || mips64 || ppc64 || s390x) && linux
 
 package main
 
diff --git a/examples/tracepoint_in_c/bpf_bpfel.go b/examples/tracepoint_in_c/bpf_bpfel.go
index 8e3858f..691d541 100644
--- a/examples/tracepoint_in_c/bpf_bpfel.go
+++ b/examples/tracepoint_in_c/bpf_bpfel.go
@@ -1,5 +1,5 @@
 // Code generated by bpf2go; DO NOT EDIT.
-//go:build 386 || amd64 || arm || arm64 || loong64 || mips64le || mipsle || ppc64le || riscv64
+//go:build (386 || amd64 || arm || arm64 || loong64 || mips64le || mipsle || ppc64le || riscv64) && linux
 
 package main
 
diff --git a/examples/tracepoint_in_c/main.go b/examples/tracepoint_in_c/main.go
index e0ab6b3..8a60d46 100644
--- a/examples/tracepoint_in_c/main.go
+++ b/examples/tracepoint_in_c/main.go
@@ -1,3 +1,5 @@
+//go:build linux
+
 // This program demonstrates attaching an eBPF program to a kernel tracepoint.
 // The eBPF program will be attached to the page allocation tracepoint and
 // prints out the number of times it has been reached. The tracepoint fields
@@ -12,7 +14,7 @@ import (
 	"github.com/cilium/ebpf/rlimit"
 )
 
-//go:generate go run github.com/cilium/ebpf/cmd/bpf2go bpf tracepoint.c -- -I../headers
+//go:generate go run github.com/cilium/ebpf/cmd/bpf2go -tags linux bpf tracepoint.c -- -I../headers
 
 const mapKey uint32 = 0
 
diff --git a/examples/tracepoint_in_go/main.go b/examples/tracepoint_in_go/main.go
index 85af8e5..a415d74 100644
--- a/examples/tracepoint_in_go/main.go
+++ b/examples/tracepoint_in_go/main.go
@@ -1,3 +1,5 @@
+//go:build linux
+
 // This program demonstrates how to attach an eBPF program to a tracepoint.
 // The program is attached to the syscall/sys_enter_openat tracepoint and
 // prints out the integer 123 every time the syscall is entered.
diff --git a/examples/uretprobe/main.go b/examples/uretprobe/main.go
index 30d1ad7..1d7ae33 100644
--- a/examples/uretprobe/main.go
+++ b/examples/uretprobe/main.go
@@ -22,7 +22,7 @@ import (
 	"github.com/cilium/ebpf/rlimit"
 )
 
-//go:generate go run github.com/cilium/ebpf/cmd/bpf2go -target amd64 -tags linux -type event bpf uretprobe.c -- -I../headers
+//go:generate go run github.com/cilium/ebpf/cmd/bpf2go -tags linux -target amd64 -type event bpf uretprobe.c -- -I../headers
 
 const (
 	// The path to the ELF binary containing the function to trace.
diff --git a/examples/xdp/bpf_bpfeb.go b/examples/xdp/bpf_bpfeb.go
index 5fbb5f0..9b5420d 100644
--- a/examples/xdp/bpf_bpfeb.go
+++ b/examples/xdp/bpf_bpfeb.go
@@ -1,5 +1,5 @@
 // Code generated by bpf2go; DO NOT EDIT.
-//go:build mips || mips64 || ppc64 || s390x
+//go:build (mips || mips64 || ppc64 || s390x) && linux
 
 package main
 
diff --git a/examples/xdp/bpf_bpfel.go b/examples/xdp/bpf_bpfel.go
index e7b326b..45810c6 100644
--- a/examples/xdp/bpf_bpfel.go
+++ b/examples/xdp/bpf_bpfel.go
@@ -1,5 +1,5 @@
 // Code generated by bpf2go; DO NOT EDIT.
-//go:build 386 || amd64 || arm || arm64 || loong64 || mips64le || mipsle || ppc64le || riscv64
+//go:build (386 || amd64 || arm || arm64 || loong64 || mips64le || mipsle || ppc64le || riscv64) && linux
 
 package main
 
diff --git a/examples/xdp/main.go b/examples/xdp/main.go
index 123513b..2b71331 100644
--- a/examples/xdp/main.go
+++ b/examples/xdp/main.go
@@ -1,3 +1,5 @@
+//go:build linux
+
 // This program demonstrates attaching an eBPF program to a network interface
 // with XDP (eXpress Data Path). The program parses the IPv4 source address
 // from packets and writes the packet count by IP to an LRU hash map.
@@ -21,7 +23,7 @@ import (
 	"github.com/cilium/ebpf/link"
 )
 
-//go:generate go run github.com/cilium/ebpf/cmd/bpf2go bpf xdp.c -- -I../headers
+//go:generate go run github.com/cilium/ebpf/cmd/bpf2go -tags linux bpf xdp.c -- -I../headers
 
 func main() {
 	if len(os.Args) < 2 {
diff --git a/features/map.go b/features/map.go
index 4b16e6a..2c68ab0 100644
--- a/features/map.go
+++ b/features/map.go
@@ -1,3 +1,5 @@
+//go:build linux
+
 package features
 
 import (
diff --git a/features/misc.go b/features/misc.go
index c039020..f07ef92 100644
--- a/features/misc.go
+++ b/features/misc.go
@@ -1,3 +1,5 @@
+//go:build linux
+
 package features
 
 import (
diff --git a/features/prog.go b/features/prog.go
index ee6f89c..e71525e 100644
--- a/features/prog.go
+++ b/features/prog.go
@@ -1,3 +1,5 @@
+//go:build linux
+
 package features
 
 import (
diff --git a/features/version.go b/features/version.go
index d54d3ea..124dcd2 100644
--- a/features/version.go
+++ b/features/version.go
@@ -1,3 +1,5 @@
+//go:build linux
+
 package features
 
 import "github.com/cilium/ebpf/internal/linux"
diff --git a/internal/epoll/poller.go b/internal/epoll/poller.go
index 733e839..76bfa77 100644
--- a/internal/epoll/poller.go
+++ b/internal/epoll/poller.go
@@ -1,3 +1,5 @@
+//go:build linux
+
 package epoll
 
 import (
diff --git a/internal/feature.go b/internal/feature.go
index 6399be0..6f64822 100644
--- a/internal/feature.go
+++ b/internal/feature.go
@@ -93,7 +93,7 @@ func NewFeatureTest(name string, fn FeatureTestFn, versions ...string) func() er
 			break
 		}
 
-		if runtime.GOOS == "linux" && !strings.ContainsRune(version, ':') {
+		if OnLinux && !strings.ContainsRune(version, ':') {
 			// Allow version numbers without a GOOS prefix on Linux.
 			ft.Version = version
 			break
diff --git a/internal/goos.go b/internal/goos.go
new file mode 100644
index 0000000..0569d5c
--- /dev/null
+++ b/internal/goos.go
@@ -0,0 +1,7 @@
+package internal
+
+import "runtime"
+
+const (
+	OnLinux = runtime.GOOS == "linux"
+)
diff --git a/internal/kallsyms/kallsyms.go b/internal/kallsyms/kallsyms.go
index a8e76a5..2cdfb56 100644
--- a/internal/kallsyms/kallsyms.go
+++ b/internal/kallsyms/kallsyms.go
@@ -8,6 +8,8 @@ import (
 	"slices"
 	"strconv"
 	"strings"
+
+	"github.com/cilium/ebpf/internal"
 )
 
 var errAmbiguousKsym = errors.New("multiple kernel symbols with the same name")
@@ -47,6 +49,10 @@ func Module(name string) (string, error) {
 // Any symbols missing in the kernel are ignored. Returns an error if multiple
 // symbols with a given name were found.
 func AssignModules(symbols map[string]string) error {
+	if !internal.OnLinux {
+		return fmt.Errorf("read /proc/kallsyms: %w", internal.ErrNotSupportedOnOS)
+	}
+
 	if len(symbols) == 0 {
 		return nil
 	}
@@ -162,6 +168,10 @@ func Address(symbol string) (uint64, error) {
 // Any symbols missing in the kernel are ignored. Returns an error if multiple
 // addresses were found for a symbol.
 func AssignAddresses(symbols map[string]uint64) error {
+	if !internal.OnLinux {
+		return fmt.Errorf("read /proc/kallsyms: %w", internal.ErrNotSupportedOnOS)
+	}
+
 	if len(symbols) == 0 {
 		return nil
 	}
diff --git a/internal/linux/auxv.go b/internal/linux/auxv.go
index 98bb5d8..07d5efe 100644
--- a/internal/linux/auxv.go
+++ b/internal/linux/auxv.go
@@ -2,8 +2,11 @@ package linux
 
 import (
 	"errors"
+	"fmt"
 	"io"
 	_ "unsafe"
+
+	"github.com/cilium/ebpf/internal"
 )
 
 type auxvPairReader interface {
@@ -47,6 +50,10 @@ func (r *auxvRuntimeReader) ReadAuxvPair() (uint64, uint64, error) {
 }
 
 func newAuxvRuntimeReader() (auxvPairReader, error) {
+	if !internal.OnLinux {
+		return nil, fmt.Errorf("read auxv from runtime: %w", internal.ErrNotSupportedOnOS)
+	}
+
 	data := runtime_getAuxv()
 
 	if len(data)%2 != 0 {
diff --git a/internal/tracefs/kprobe.go b/internal/tracefs/kprobe.go
index 062bef9..bdadf4d 100644
--- a/internal/tracefs/kprobe.go
+++ b/internal/tracefs/kprobe.go
@@ -113,6 +113,10 @@ func sanitizeTracefsPath(path ...string) (string, error) {
 // but may be also be available at /sys/kernel/debug/tracing if debugfs is mounted.
 // The available tracefs paths will depends on distribution choices.
 var getTracefsPath = sync.OnceValues(func() (string, error) {
+	if !internal.OnLinux {
+		return "", fmt.Errorf("tracefs: %w", internal.ErrNotSupportedOnOS)
+	}
+
 	for _, p := range []struct {
 		path   string
 		fsType int64
diff --git a/internal/unix/error.go b/internal/unix/error.go
new file mode 100644
index 0000000..48017c1
--- /dev/null
+++ b/internal/unix/error.go
@@ -0,0 +1,23 @@
+package unix
+
+import (
+	"fmt"
+	"runtime"
+	"strings"
+
+	"github.com/cilium/ebpf/internal"
+)
+
+// errNonLinux returns an error which wraps [internal.ErrNotSupportedOnOS] and
+// includes the name of the calling function.
+func errNonLinux() error {
+	name := "unknown"
+	pc, _, _, ok := runtime.Caller(1)
+	if ok {
+		name = runtime.FuncForPC(pc).Name()
+		if pos := strings.LastIndexByte(name, '.'); pos != -1 {
+			name = name[pos+1:]
+		}
+	}
+	return fmt.Errorf("unix: %s: %w", name, internal.ErrNotSupportedOnOS)
+}
diff --git a/internal/unix/strings_other.go b/internal/unix/strings_other.go
new file mode 100644
index 0000000..49e1fe2
--- /dev/null
+++ b/internal/unix/strings_other.go
@@ -0,0 +1,11 @@
+//go:build !linux && !windows
+
+package unix
+
+func BytePtrFromString(s string) (*byte, error) {
+	return nil, errNonLinux()
+}
+
+func ByteSliceToString(s []byte) string {
+	return ""
+}
diff --git a/internal/unix/strings_windows.go b/internal/unix/strings_windows.go
new file mode 100644
index 0000000..7e3d4da
--- /dev/null
+++ b/internal/unix/strings_windows.go
@@ -0,0 +1,19 @@
+package unix
+
+import (
+	"syscall"
+
+	"golang.org/x/sys/windows"
+)
+
+func BytePtrFromString(s string) (*byte, error) {
+	p, err := windows.BytePtrFromString(s)
+	if err == syscall.EINVAL {
+		err = EINVAL
+	}
+	return p, err
+}
+
+func ByteSliceToString(s []byte) string {
+	return windows.ByteSliceToString(s)
+}
diff --git a/internal/unix/types_other.go b/internal/unix/types_other.go
index 06cc3a0..806c6e5 100644
--- a/internal/unix/types_other.go
+++ b/internal/unix/types_other.go
@@ -3,13 +3,9 @@
 package unix
 
 import (
-	"fmt"
-	"runtime"
 	"syscall"
 )
 
-var errNonLinux = fmt.Errorf("unsupported platform %s/%s", runtime.GOOS, runtime.GOARCH)
-
 // Errnos are distinct and non-zero.
 const (
 	ENOENT syscall.Errno = iota + 1
@@ -142,23 +138,23 @@ func Syscall(trap, a1, a2, a3 uintptr) (r1, r2 uintptr, err syscall.Errno) {
 }
 
 func PthreadSigmask(how int, set, oldset *Sigset_t) error {
-	return errNonLinux
+	return errNonLinux()
 }
 
 func FcntlInt(fd uintptr, cmd, arg int) (int, error) {
-	return -1, errNonLinux
+	return -1, errNonLinux()
 }
 
 func IoctlSetInt(fd int, req uint, value int) error {
-	return errNonLinux
+	return errNonLinux()
 }
 
 func Statfs(path string, buf *Statfs_t) error {
-	return errNonLinux
+	return errNonLinux()
 }
 
 func Close(fd int) (err error) {
-	return errNonLinux
+	return errNonLinux()
 }
 
 type EpollEvent struct {
@@ -168,23 +164,23 @@ type EpollEvent struct {
 }
 
 func EpollWait(epfd int, events []EpollEvent, msec int) (n int, err error) {
-	return 0, errNonLinux
+	return 0, errNonLinux()
 }
 
 func EpollCtl(epfd int, op int, fd int, event *EpollEvent) (err error) {
-	return errNonLinux
+	return errNonLinux()
 }
 
 func Eventfd(initval uint, flags int) (fd int, err error) {
-	return 0, errNonLinux
+	return 0, errNonLinux()
 }
 
 func Write(fd int, p []byte) (n int, err error) {
-	return 0, errNonLinux
+	return 0, errNonLinux()
 }
 
 func EpollCreate1(flag int) (fd int, err error) {
-	return 0, errNonLinux
+	return 0, errNonLinux()
 }
 
 type PerfEventMmapPage struct {
@@ -214,15 +210,15 @@ type PerfEventMmapPage struct {
 }
 
 func SetNonblock(fd int, nonblocking bool) (err error) {
-	return errNonLinux
+	return errNonLinux()
 }
 
 func Mmap(fd int, offset int64, length int, prot int, flags int) (data []byte, err error) {
-	return []byte{}, errNonLinux
+	return []byte{}, errNonLinux()
 }
 
 func Munmap(b []byte) (err error) {
-	return errNonLinux
+	return errNonLinux()
 }
 
 type PerfEventAttr struct {
@@ -247,7 +243,7 @@ type PerfEventAttr struct {
 }
 
 func PerfEventOpen(attr *PerfEventAttr, pid int, cpu int, groupFd int, flags int) (fd int, err error) {
-	return 0, errNonLinux
+	return 0, errNonLinux()
 }
 
 type Utsname struct {
@@ -256,7 +252,7 @@ type Utsname struct {
 }
 
 func Uname(buf *Utsname) (err error) {
-	return errNonLinux
+	return errNonLinux()
 }
 
 func Getpid() int {
@@ -268,35 +264,27 @@ func Gettid() int {
 }
 
 func Tgkill(tgid int, tid int, sig syscall.Signal) (err error) {
-	return errNonLinux
-}
-
-func BytePtrFromString(s string) (*byte, error) {
-	return nil, errNonLinux
-}
-
-func ByteSliceToString(s []byte) string {
-	return ""
+	return errNonLinux()
 }
 
 func Renameat2(olddirfd int, oldpath string, newdirfd int, newpath string, flags uint) error {
-	return errNonLinux
+	return errNonLinux()
 }
 
 func Prlimit(pid, resource int, new, old *Rlimit) error {
-	return errNonLinux
+	return errNonLinux()
 }
 
 func Open(path string, mode int, perm uint32) (int, error) {
-	return -1, errNonLinux
+	return -1, errNonLinux()
 }
 
 func Fstat(fd int, stat *Stat_t) error {
-	return errNonLinux
+	return errNonLinux()
 }
 
 func SetsockoptInt(fd, level, opt, value int) error {
-	return errNonLinux
+	return errNonLinux()
 }
 
 type CPUSet struct{}
@@ -304,9 +292,9 @@ type CPUSet struct{}
 func (*CPUSet) Set(int) {}
 
 func SchedSetaffinity(pid int, set *CPUSet) error {
-	return errNonLinux
+	return errNonLinux()
 }
 
 func SchedGetaffinity(pid int, set *CPUSet) error {
-	return errNonLinux
+	return errNonLinux()
 }
diff --git a/perf/reader.go b/perf/reader.go
index 3c3d569..113148b 100644
--- a/perf/reader.go
+++ b/perf/reader.go
@@ -1,3 +1,5 @@
+//go:build linux
+
 package perf
 
 import (
diff --git a/perf/ring.go b/perf/ring.go
index 63555f3..426f7e1 100644
--- a/perf/ring.go
+++ b/perf/ring.go
@@ -1,3 +1,5 @@
+//go:build linux
+
 package perf
 
 import (
diff --git a/ringbuf/reader.go b/ringbuf/reader.go
index 11ef93b..202738e 100644
--- a/ringbuf/reader.go
+++ b/ringbuf/reader.go
@@ -1,3 +1,5 @@
+//go:build linux
+
 package ringbuf
 
 import (
diff --git a/ringbuf/ring.go b/ringbuf/ring.go
index 8f75bba..c78a76f 100644
--- a/ringbuf/ring.go
+++ b/ringbuf/ring.go
@@ -1,3 +1,5 @@
+//go:build linux
+
 package ringbuf
 
 import (


Test patch:
diff --git a/cmd/bpf2go/gen/compile_test.go b/cmd/bpf2go/gen/compile_test.go
index f5aeb7d..a49d13d 100644
--- a/cmd/bpf2go/gen/compile_test.go
+++ b/cmd/bpf2go/gen/compile_test.go
@@ -1,3 +1,5 @@
+//go:build !windows
+
 package gen
 
 import (
diff --git a/cmd/bpf2go/gen/output_test.go b/cmd/bpf2go/gen/output_test.go
index 75d1221..3859087 100644
--- a/cmd/bpf2go/gen/output_test.go
+++ b/cmd/bpf2go/gen/output_test.go
@@ -1,3 +1,5 @@
+//go:build !windows
+
 package gen
 
 import (
diff --git a/cmd/bpf2go/gen/target_test.go b/cmd/bpf2go/gen/target_test.go
index 021b9dd..c0fe2ca 100644
--- a/cmd/bpf2go/gen/target_test.go
+++ b/cmd/bpf2go/gen/target_test.go
@@ -1,3 +1,5 @@
+//go:build !windows
+
 package gen
 
 import (
diff --git a/cmd/bpf2go/gen/types_test.go b/cmd/bpf2go/gen/types_test.go
index 286705a..13fec24 100644
--- a/cmd/bpf2go/gen/types_test.go
+++ b/cmd/bpf2go/gen/types_test.go
@@ -1,3 +1,5 @@
+//go:build !windows
+
 package gen
 
 import (
diff --git a/cmd/bpf2go/main_test.go b/cmd/bpf2go/main_test.go
index eefdb42..a7c9605 100644
--- a/cmd/bpf2go/main_test.go
+++ b/cmd/bpf2go/main_test.go
@@ -1,3 +1,5 @@
+//go:build !windows
+
 package main
 
 import (
diff --git a/cmd/bpf2go/makedep_test.go b/cmd/bpf2go/makedep_test.go
index 6dff267..5f87988 100644
--- a/cmd/bpf2go/makedep_test.go
+++ b/cmd/bpf2go/makedep_test.go
@@ -1,3 +1,5 @@
+//go:build !windows
+
 package main
 
 import (
diff --git a/cmd/bpf2go/test/api_test.go b/cmd/bpf2go/test/api_test.go
index 7262912..9fe1519 100644
--- a/cmd/bpf2go/test/api_test.go
+++ b/cmd/bpf2go/test/api_test.go
@@ -1,3 +1,5 @@
+//go:build !windows
+
 package test
 
 import (
diff --git a/cmd/bpf2go/test/doc.go b/cmd/bpf2go/test/doc.go
index 83c64e1..92ff2e2 100644
--- a/cmd/bpf2go/test/doc.go
+++ b/cmd/bpf2go/test/doc.go
@@ -2,4 +2,4 @@
 // specific API.
 package test
 
-//go:generate go run github.com/cilium/ebpf/cmd/bpf2go test ../testdata/minimal.c
+//go:generate go run github.com/cilium/ebpf/cmd/bpf2go -tags linux test ../testdata/minimal.c
diff --git a/cmd/bpf2go/test/test_bpfeb.go b/cmd/bpf2go/test/test_bpfeb.go
index be2f2e6..a1d0e54 100644
--- a/cmd/bpf2go/test/test_bpfeb.go
+++ b/cmd/bpf2go/test/test_bpfeb.go
@@ -1,5 +1,5 @@
 // Code generated by bpf2go; DO NOT EDIT.
-//go:build mips || mips64 || ppc64 || s390x
+//go:build (mips || mips64 || ppc64 || s390x) && linux
 
 package test
 
diff --git a/cmd/bpf2go/test/test_bpfel.go b/cmd/bpf2go/test/test_bpfel.go
index 4fb0c22..a990bcc 100644
--- a/cmd/bpf2go/test/test_bpfel.go
+++ b/cmd/bpf2go/test/test_bpfel.go
@@ -1,5 +1,5 @@
 // Code generated by bpf2go; DO NOT EDIT.
-//go:build 386 || amd64 || arm || arm64 || loong64 || mips64le || mipsle || ppc64le || riscv64
+//go:build (386 || amd64 || arm || arm64 || loong64 || mips64le || mipsle || ppc64le || riscv64) && linux
 
 package test
 
diff --git a/cmd/bpf2go/tools_test.go b/cmd/bpf2go/tools_test.go
index b91ecc4..4319653 100644
--- a/cmd/bpf2go/tools_test.go
+++ b/cmd/bpf2go/tools_test.go
@@ -1,3 +1,5 @@
+//go:build !windows
+
 package main
 
 import (
diff --git a/docs/examples/docs_test.go b/docs/examples/docs_test.go
index 96b8864..c50507f 100644
--- a/docs/examples/docs_test.go
+++ b/docs/examples/docs_test.go
@@ -1,3 +1,5 @@
+//go:build linux
+
 package examples
 
 import (
diff --git a/docs/examples/features_test.go b/docs/examples/features_test.go
index b536c16..8cd6a83 100644
--- a/docs/examples/features_test.go
+++ b/docs/examples/features_test.go
@@ -1,3 +1,5 @@
+//go:build linux
+
 package examples
 
 import (
diff --git a/docs/examples/rlimit_test.go b/docs/examples/rlimit_test.go
index e8deae5..5eb52ea 100644
--- a/docs/examples/rlimit_test.go
+++ b/docs/examples/rlimit_test.go
@@ -1,3 +1,5 @@
+//go:build linux
+
 package examples
 
 // DocRlimit {
diff --git a/features/map_test.go b/features/map_test.go
index f2a0a44..0071a40 100644
--- a/features/map_test.go
+++ b/features/map_test.go
@@ -1,3 +1,5 @@
+//go:build linux
+
 package features
 
 import (
diff --git a/features/misc_test.go b/features/misc_test.go
index 7e26103..4c8d4e8 100644
--- a/features/misc_test.go
+++ b/features/misc_test.go
@@ -1,3 +1,5 @@
+//go:build linux
+
 package features
 
 import (
diff --git a/features/prog_test.go b/features/prog_test.go
index 16e4b46..8fe5beb 100644
--- a/features/prog_test.go
+++ b/features/prog_test.go
@@ -1,3 +1,5 @@
+//go:build linux
+
 package features
 
 import (
diff --git a/internal/epoll/poller_test.go b/internal/epoll/poller_test.go
index cac4493..61bf65d 100644
--- a/internal/epoll/poller_test.go
+++ b/internal/epoll/poller_test.go
@@ -1,3 +1,5 @@
+//go:build linux
+
 package epoll
 
 import (
diff --git a/internal/feature_test.go b/internal/feature_test.go
index cd57eb4..346e857 100644
--- a/internal/feature_test.go
+++ b/internal/feature_test.go
@@ -83,7 +83,7 @@ func TestFeatureTestNotSupportedOnOS(t *testing.T) {
 	qt.Assert(t, qt.IsNotNil(NewFeatureTest("foo", fn)()))
 	qt.Assert(t, qt.ErrorIs(NewFeatureTest("foo", fn, "froz:1.0.0")(), ErrNotSupportedOnOS))
 	qt.Assert(t, qt.ErrorIs(NewFeatureTest("foo", fn, runtime.GOOS+":1.0")(), sentinel))
-	if runtime.GOOS == "linux" {
+	if OnLinux {
 		qt.Assert(t, qt.ErrorIs(NewFeatureTest("foo", fn, "1.0")(), sentinel))
 	}
 }
diff --git a/internal/kallsyms/kallsyms_test.go b/internal/kallsyms/kallsyms_test.go
index 81c6d4f..9b111c3 100644
--- a/internal/kallsyms/kallsyms_test.go
+++ b/internal/kallsyms/kallsyms_test.go
@@ -6,6 +6,9 @@ import (
 	"testing"
 
 	"github.com/go-quicktest/qt"
+
+	"github.com/cilium/ebpf/internal"
+	"github.com/cilium/ebpf/internal/testutils"
 )
 
 var syms = []byte(`0000000000000001 t hid_generic_probe	[hid_generic]
@@ -37,12 +40,8 @@ func TestParseSyms(t *testing.T) {
 }
 
 func TestParseProcKallsyms(t *testing.T) {
-	f, err := os.Open("/proc/kallsyms")
-	qt.Assert(t, qt.IsNil(err))
-	defer f.Close()
-
 	// Read up to 50k symbols from kallsyms to avoid a slow test.
-	r := newReader(f)
+	r := newReader(mustOpenProcKallsyms(t))
 	for i := 0; r.Line() && i < 50_000; i++ {
 		s, err, skip := parseSymbol(r, nil)
 		qt.Assert(t, qt.IsNil(err))
@@ -53,12 +52,14 @@ func TestParseProcKallsyms(t *testing.T) {
 }
 
 func TestAssignModulesCaching(t *testing.T) {
-	qt.Assert(t, qt.IsNil(AssignModules(
+	err := AssignModules(
 		map[string]string{
 			"bpf_perf_event_output": "",
 			"foo":                   "",
 		},
-	)))
+	)
+	testutils.SkipIfNotSupportedOnOS(t, err)
+	qt.Assert(t, qt.IsNil(err))
 
 	// Can't assume any kernel modules are loaded, but this symbol should at least
 	// exist in the kernel. There is no semantic difference between a missing
@@ -92,12 +93,14 @@ func TestAssignModules(t *testing.T) {
 }
 
 func TestAssignAddressesCaching(t *testing.T) {
-	qt.Assert(t, qt.IsNil(AssignAddresses(
+	err := AssignAddresses(
 		map[string]uint64{
 			"bpf_perf_event_output": 0,
 			"foo":                   0,
 		},
-	)))
+	)
+	testutils.SkipIfNotSupportedOnOS(t, err)
+	qt.Assert(t, qt.IsNil(err))
 
 	v, ok := symAddrs.Load("bpf_perf_event_output")
 	qt.Assert(t, qt.IsTrue(ok))
@@ -133,8 +136,7 @@ func BenchmarkSymbolKmods(b *testing.B) {
 	b.ReportAllocs()
 	for i := 0; i < b.N; i++ {
 		b.StopTimer()
-		f, err := os.Open("/proc/kallsyms")
-		qt.Assert(b, qt.IsNil(err))
+		f := mustOpenProcKallsyms(b)
 		want := map[string]string{
 			"bpf_trace_vprintk":     "",
 			"bpf_send_signal":       "",
@@ -147,8 +149,6 @@ func BenchmarkSymbolKmods(b *testing.B) {
 		if err := assignModules(f, want); err != nil {
 			b.Fatal(err)
 		}
-
-		f.Close()
 	}
 }
 
@@ -157,8 +157,7 @@ func BenchmarkAssignAddresses(b *testing.B) {
 	b.ReportAllocs()
 	for i := 0; i < b.N; i++ {
 		b.StopTimer()
-		f, err := os.Open("/proc/kallsyms")
-		qt.Assert(b, qt.IsNil(err))
+		f := mustOpenProcKallsyms(b)
 		want := map[string]uint64{
 			"bpf_trace_vprintk":     0,
 			"bpf_send_signal":       0,
@@ -171,7 +170,18 @@ func BenchmarkAssignAddresses(b *testing.B) {
 		if err := assignAddresses(f, want); err != nil {
 			b.Fatal(err)
 		}
+	}
+}
 
-		f.Close()
+func mustOpenProcKallsyms(tb testing.TB) *os.File {
+	tb.Helper()
+
+	if !internal.OnLinux {
+		tb.Skip("/proc/kallsyms is a Linux concept")
 	}
+
+	f, err := os.Open("/proc/kallsyms")
+	qt.Assert(tb, qt.IsNil(err))
+	tb.Cleanup(func() { f.Close() })
+	return f
 }
diff --git a/internal/linux/auxv_test.go b/internal/linux/auxv_test.go
index 14ab0eb..32bf0f6 100644
--- a/internal/linux/auxv_test.go
+++ b/internal/linux/auxv_test.go
@@ -72,6 +72,7 @@ func newDefaultAuxvFileReader() (auxvPairReader, error) {
 
 func TestAuxvBothSourcesEqual(t *testing.T) {
 	runtimeBased, err := newAuxvRuntimeReader()
+	skipIfNotSupportedOnOS(t, err)
 	if err != nil {
 		t.Fatal(err)
 	}
diff --git a/internal/linux/helper_test.go b/internal/linux/helper_test.go
new file mode 100644
index 0000000..3296aba
--- /dev/null
+++ b/internal/linux/helper_test.go
@@ -0,0 +1,22 @@
+package linux
+
+import (
+	"errors"
+	"testing"
+
+	"github.com/cilium/ebpf/internal"
+)
+
+// skipIfNotSupportedOnOS is a copy of testutils.SkipIfNotSupported to avoid
+// a circular dependency.
+func skipIfNotSupportedOnOS(tb testing.TB, err error) {
+	tb.Helper()
+
+	if err == internal.ErrNotSupportedOnOS {
+		tb.Fatal("Unwrapped ErrNotSupportedOnOS")
+	}
+
+	if errors.Is(err, internal.ErrNotSupportedOnOS) {
+		tb.Skip(err.Error())
+	}
+}
diff --git a/internal/linux/statfs_test.go b/internal/linux/statfs_test.go
index d1fed82..69493e7 100644
--- a/internal/linux/statfs_test.go
+++ b/internal/linux/statfs_test.go
@@ -17,6 +17,7 @@ func TestFSType(t *testing.T) {
 		{"/sys/fs/bpf", unix.BPF_FS_MAGIC},
 	} {
 		fst, err := FSType(fs.path)
+		skipIfNotSupportedOnOS(t, err)
 		qt.Assert(t, qt.IsNil(err))
 		qt.Assert(t, qt.Equals(fst, fs.magic))
 	}
diff --git a/internal/linux/vdso_test.go b/internal/linux/vdso_test.go
index b50c80c..947ad88 100644
--- a/internal/linux/vdso_test.go
+++ b/internal/linux/vdso_test.go
@@ -53,6 +53,7 @@ func TestAuxvNoVDSO(t *testing.T) {
 
 func TestVDSOVersion(t *testing.T) {
 	_, err := vdsoVersion()
+	skipIfNotSupportedOnOS(t, err)
 	qt.Assert(t, qt.IsNil(err))
 }
 
diff --git a/internal/linux/version_test.go b/internal/linux/version_test.go
index 8c122dc..9d14e75 100644
--- a/internal/linux/version_test.go
+++ b/internal/linux/version_test.go
@@ -8,11 +8,13 @@ import (
 
 func TestCurrentKernelVersion(t *testing.T) {
 	_, err := KernelVersion()
+	skipIfNotSupportedOnOS(t, err)
 	qt.Assert(t, qt.IsNil(err))
 }
 
 func TestKernelRelease(t *testing.T) {
 	r, err := KernelRelease()
+	skipIfNotSupportedOnOS(t, err)
 	if err != nil {
 		t.Fatal(err)
 	}
diff --git a/internal/tracefs/kprobe_test.go b/internal/tracefs/kprobe_test.go
index e0d6e9b..78b60c6 100644
--- a/internal/tracefs/kprobe_test.go
+++ b/internal/tracefs/kprobe_test.go
@@ -6,6 +6,8 @@ import (
 	"testing"
 
 	"github.com/go-quicktest/qt"
+
+	"github.com/cilium/ebpf/internal/testutils"
 )
 
 // Global symbol, present on all tested kernels.
@@ -63,6 +65,7 @@ func TestNewEvent(t *testing.T) {
 			args.Group, _ = RandomGroup("ebpftest")
 
 			evt, err := NewEvent(args)
+			testutils.SkipIfNotSupportedOnOS(t, err)
 			qt.Assert(t, qt.IsNil(err))
 			defer evt.Close()
 
diff --git a/internal/tracefs/perf_event_test.go b/internal/tracefs/perf_event_test.go
index 59e933b..1164ef7 100644
--- a/internal/tracefs/perf_event_test.go
+++ b/internal/tracefs/perf_event_test.go
@@ -3,20 +3,24 @@ package tracefs
 import (
 	"errors"
 	"fmt"
+	"os"
 	"testing"
 
 	"github.com/go-quicktest/qt"
+
+	"github.com/cilium/ebpf/internal/testutils"
 )
 
 func TestEventID(t *testing.T) {
-
 	eid, err := EventID("syscalls", "sys_enter_mmap")
+	testutils.SkipIfNotSupportedOnOS(t, err)
 	qt.Assert(t, qt.IsNil(err))
 	qt.Assert(t, qt.Not(qt.Equals(eid, 0)))
 }
 
 func TestSanitizePath(t *testing.T) {
 	_, err := sanitizeTracefsPath("../escaped")
+	testutils.SkipIfNotSupportedOnOS(t, err)
 	if !errors.Is(err, ErrInvalidInput) {
 		t.Errorf("expected error %s, got: %s", ErrInvalidInput, err)
 	}
@@ -80,6 +84,9 @@ func TestSanitizeIdentifier(t *testing.T) {
 }
 
 func TestGetTracefsPath(t *testing.T) {
-	_, err := getTracefsPath()
+	path, err := getTracefsPath()
+	testutils.SkipIfNotSupportedOnOS(t, err)
+	qt.Assert(t, qt.IsNil(err))
+	_, err = os.Stat(path)
 	qt.Assert(t, qt.IsNil(err))
 }
diff --git a/internal/unix/error_test.go b/internal/unix/error_test.go
new file mode 100644
index 0000000..b602eb5
--- /dev/null
+++ b/internal/unix/error_test.go
@@ -0,0 +1,15 @@
+package unix
+
+import (
+	"testing"
+
+	"github.com/go-quicktest/qt"
+
+	"github.com/cilium/ebpf/internal"
+)
+
+func TestErrNonLinux(t *testing.T) {
+	err := errNonLinux()
+	qt.Assert(t, qt.StringContains(err.Error(), t.Name()))
+	qt.Assert(t, qt.ErrorIs(err, internal.ErrNotSupportedOnOS))
+}
diff --git a/perf/reader_test.go b/perf/reader_test.go
index a2bf677..e197710 100644
--- a/perf/reader_test.go
+++ b/perf/reader_test.go
@@ -1,3 +1,5 @@
+//go:build linux
+
 package perf
 
 import (
diff --git a/perf/ring_test.go b/perf/ring_test.go
index fa9e70c..f35adf9 100644
--- a/perf/ring_test.go
+++ b/perf/ring_test.go
@@ -1,3 +1,5 @@
+//go:build linux
+
 package perf
 
 import (
diff --git a/ringbuf/reader_test.go b/ringbuf/reader_test.go
index e885a90..477edaa 100644
--- a/ringbuf/reader_test.go
+++ b/ringbuf/reader_test.go
@@ -1,3 +1,5 @@
+//go:build linux
+
 package ringbuf
 
 import (