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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 12 additions & 4 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,9 @@ jobs:

- uses: actions/setup-dotnet@v4
with:
dotnet-version: '8.0.x'
dotnet-version: |
8.0.x
10.0.x

- name: Build and pack
run: sh ./build/build_runtime_macos_packages.sh
Expand Down Expand Up @@ -80,7 +82,9 @@ jobs:

- uses: actions/setup-dotnet@v4
with:
dotnet-version: '8.0.x'
dotnet-version: |
8.0.x
10.0.x

- name: Collect runtime packages into a local feed
uses: actions/download-artifact@v4
Expand Down Expand Up @@ -115,7 +119,9 @@ jobs:

- uses: actions/setup-dotnet@v4
with:
dotnet-version: '8.0.x'
dotnet-version: |
8.0.x
10.0.x

- name: Collect runtime packages into a local feed
uses: actions/download-artifact@v4
Expand Down Expand Up @@ -152,7 +158,9 @@ jobs:
steps:
- uses: actions/setup-dotnet@v4
with:
dotnet-version: '8.0.x'
dotnet-version: |
8.0.x
10.0.x

- uses: actions/download-artifact@v4
with:
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,6 @@ runtime.*.liblouis/runtimes/

# Rider / ReSharper per-user settings
*.DotSettings.user

# Worktrees created by spawned Claude Code sessions
.claude/worktrees/
63 changes: 42 additions & 21 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,28 @@
FROM --platform=linux/amd64 mcr.microsoft.com/dotnet/sdk:8.0-jammy AS base
LABEL org.opencontainers.image.source=https://github.com/Notalib/LibLouis.NET/

RUN apt-get update && \
apt-get upgrade -y && \
apt-get install -y --no-install-recommends \
build-essential \
ca-certificates \
curl \
m4 \
xz-utils \
&& rm -rf /var/lib/apt/lists/*
# Retried, because a single apt-get run is a coin flip against archive.ubuntu.com: the index and
# the pool are not updated atomically, so a package version can be listed after it has been removed
# and the fetch 404s. That is what it did. Each attempt refreshes the index first, since a newer
# index is usually what resolves it. The explicit ok check matters: without it a loop that never
# succeeds still falls through and the layer builds with nothing installed.
RUN set -eu; \
ok=0; \
for attempt in 1 2 3; do \
if apt-get update && apt-get upgrade -y && apt-get install -y --no-install-recommends \
build-essential \
ca-certificates \
curl \
m4 \
xz-utils \
llvm; then \
ok=1; break; \
fi; \
echo "apt attempt $attempt failed, retrying" >&2; \
sleep 10; \
done; \
[ "$ok" = 1 ] || exit 1; \
rm -rf /var/lib/apt/lists/*

ENV PACKAGE_OUTPUT_DIR=/packages
WORKDIR /source
Expand All @@ -37,18 +50,26 @@ WORKDIR /source
# The five targets Ubuntu has cross compilers for.
FROM base AS gcc-targets

RUN apt-get update && \
apt-get install -y --no-install-recommends \
gcc-i686-linux-gnu \
gcc-aarch64-linux-gnu \
gcc-mingw-w64-i686 \
gcc-mingw-w64-x86-64 \
# The cross gcc packages only Recommend their target libc, so with
# --no-install-recommends they install a compiler that cannot link. Name them explicitly
# rather than dropping the flag, so the requirement is visible.
libc6-dev-i386-cross \
libc6-dev-arm64-cross \
&& rm -rf /var/lib/apt/lists/*
# The cross gcc packages only Recommend their target libc, so with --no-install-recommends they
# install a compiler that cannot link. Named explicitly rather than dropping the flag, so the
# requirement is visible. Retried for the same reason as the base stage.
RUN set -eu; \
ok=0; \
for attempt in 1 2 3; do \
if apt-get update && apt-get install -y --no-install-recommends \
gcc-i686-linux-gnu \
gcc-aarch64-linux-gnu \
gcc-mingw-w64-i686 \
gcc-mingw-w64-x86-64 \
libc6-dev-i386-cross \
libc6-dev-arm64-cross; then \
ok=1; break; \
fi; \
echo "apt attempt $attempt failed, retrying" >&2; \
sleep 10; \
done; \
[ "$ok" = 1 ] || exit 1; \
rm -rf /var/lib/apt/lists/*

COPY . /source
RUN sh ./build/build_runtime_packages.sh gcc
Expand Down
7 changes: 4 additions & 3 deletions LibLouis.NET.Test/LibLouis.NET.Test.csproj
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<TargetFrameworks>net8.0;net10.0</TargetFrameworks>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
Expand All @@ -12,8 +12,9 @@
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.2" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.2" Condition="'$(TargetFramework)' == 'net8.0'" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.10" Condition="'$(TargetFramework)' == 'net10.0'" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.8.1" />
<PackageReference Include="xunit" Version="2.9.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="3.0.2">
<PrivateAssets>all</PrivateAssets>
Expand Down
22 changes: 15 additions & 7 deletions LibLouis.NET/LibLouis.NET.csproj
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<TargetFrameworks>net8.0;net10.0</TargetFrameworks>
<Nullable>enable</Nullable>
<GeneratePackageOnBuild>True</GeneratePackageOnBuild>
<Authors>Nota</Authors>
Expand All @@ -20,8 +20,12 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.2" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.2" />
<!-- Track the extensions packages to the target framework rather than pinning every target to
the net8.0 versions, so a net10.0 consumer does not drag an older set into its graph. -->
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.2" Condition="'$(TargetFramework)' == 'net8.0'" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.2" Condition="'$(TargetFramework)' == 'net8.0'" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.10" Condition="'$(TargetFramework)' == 'net10.0'" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.10" Condition="'$(TargetFramework)' == 'net10.0'" />
<!--
Lower bound is the version this repository currently builds, so restore does not warn about
a non-existent 3.0.0 (NU1603). LiblouisPackageVersion comes from Directory.Build.props.
Expand All @@ -30,10 +34,14 @@
</ItemGroup>

<ItemGroup>
<None Update="LICENSE">
<PackagePath>\</PackagePath>
<Pack>True</Pack>
</None>
<!--
Two forms on purpose. Packing a multi-targeted project runs the cross-targeting outer build,
which has no default None items, so Update there matches nothing and the licence silently
never reaches the package (NU5030). The inner builds do have the item, where a second Include
would be a duplicate.
-->
<None Include="LICENSE" Pack="True" PackagePath="\" Condition="'$(TargetFramework)' == ''" />
<None Update="LICENSE" Pack="True" PackagePath="\" Condition="'$(TargetFramework)' != ''" />
</ItemGroup>

</Project>
35 changes: 35 additions & 0 deletions PACKAGING.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,41 @@ Worth re-checking after a toolchain or upstream bump. The binaries should import
llvm-readobj --coff-imports liblouis.dll | grep Name:
```

### Post-build verification

Every native binary is inspected before it is packed, by `build/verify_native_binary.sh`, called
from `pack_runtime_package`. A binary that fails never becomes a package.

| Check | Why |
| --- | --- |
| Architecture matches the RID | A mis-targeted binary restores fine and never loads. |
| Every P/Invoke symbol is exported | Otherwise `EntryPointNotFoundException` at first use. |
| No dependency outside an allowlist | Catches the `libgcc_s_dw2-1.dll` class of bug. |
| Max `GLIBC_` version within the floor | The floor decides which distributions can consume the packages, and it is a property of the build image. |

The expected symbols are read out of the `EntryPoint` attributes in `LibLouis.NET/NativeMethod.cs`
rather than listed in the script, so the check cannot drift away from what the wrapper actually
imports.

The glibc floor is `MAX_GLIBC` in the script, currently 2.34, which covers RHEL 9, Debian 12 and
Ubuntu 22.04 and later. `linux-x86` sits exactly on it. Raising it drops support for older
distributions, so it should be a deliberate decision rather than a side effect of bumping the base
image.

Run it by hand against an extracted package to audit a published one:

```bash
sh build/verify_native_binary.sh win-x64 runtimes/win-x64/native/liblouis.dll
```

It needs tools that can read the format being checked. The container has them. On macOS,
`brew install llvm` covers all three formats, since llvm-readelf, llvm-nm and llvm-readobj read ELF,
PE and Mach-O alike; the script finds them under Homebrew's keg-only prefix. Without that, only the
macOS RIDs can be checked locally, because BSD nm has no -D and cannot read ELF.

`SKIP_NATIVE_VERIFICATION=1` bypasses it, which is only reasonable when deliberately building
something the checks were not written for.

### Parallel make

The Linux and macOS targets build with `make -j`. The Windows targets deliberately do not.
Expand Down
10 changes: 10 additions & 0 deletions build/common.sh
Original file line number Diff line number Diff line change
Expand Up @@ -88,8 +88,18 @@ stage_tables() {

# Pack a per-RID runtime package. The native binary must already be staged into
# runtime.<rid>.liblouis/runtimes/<rid>/native/, which RuntimePackage.props verifies.
#
# The binary is inspected before packing rather than after, so a bad build never becomes a package
# at all. Set SKIP_NATIVE_VERIFICATION=1 to bypass, which is only reasonable when deliberately
# building something the checks are not written for.
pack_runtime_package() {
rid=$1

if [ -z "${SKIP_NATIVE_VERIFICATION:-}" ]; then
binary=$(find "$REPO_ROOT/runtime.$rid.liblouis/runtimes/$rid/native" -type f | head -n 1)
sh "$REPO_ROOT/build/verify_native_binary.sh" "$rid" "$binary"
fi

mkdir -p "$PACKAGE_OUTPUT_DIR"
dotnet pack "$REPO_ROOT/runtime.$rid.liblouis/runtime.$rid.liblouis.csproj" \
--configuration Release \
Expand Down
Loading
Loading