Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cannot get cross-targeting to work w/Native AOT #88971

Closed
richlander opened this issue Jul 16, 2023 · 22 comments
Closed

Cannot get cross-targeting to work w/Native AOT #88971

richlander opened this issue Jul 16, 2023 · 22 comments

Comments

@richlander
Copy link
Member

richlander commented Jul 16, 2023

Following the instructions at:

My Dockerfile: https://github.com/richlander/dotnet-docker/blob/dotnet-8-samples/samples/releasesapi-aot/Dockerfile.cross-target

Host: Linux kelowna 6.1.38-1-MANJARO #1 SMP PREEMPT_DYNAMIC Wed Jul 5 23:49:30 UTC 2023 x86_64 GNU/Linux

Docker build: docker build --pull -t app -f Dockerfile.cross-target .

I get this error:

  Generating native code
  ILC: Method '[releasesapi]Program.<Main>$(string[])' will always throw because: Failed to load assembly 'Microsoft.AspNetCore'
  /usr/bin/ld.bfd: unrecognised emulation mode: aarch64linux
  Supported emulations: elf_x86_64 elf32_x86_64 elf_i386 elf_iamcu elf_l1om elf_k1om
clang-16 : error : linker command failed with exit code 1 (use -v to see invocation) [/source/releasesapi.csproj]
/root/.nuget/packages/microsoft.dotnet.ilcompiler/8.0.0-preview.6.23329.7/build/Microsoft.NETCore.Native.targets(364,5): error MSB3073: The command ""clang-16" "obj/Release/net8.0/linux-arm64/native/releasesapi.o" -o "bin/Release/net8.0/linux-arm64/native/releasesapi" -fuse-ld=bfd /root/.nuget/packages/runtime.linux-arm64.microsoft.dotnet.ilcompiler/8.0.0-preview.6.23329.7/sdk/libbootstrapper.a /root/.nuget/packages/runtime.linux-arm64.microsoft.dotnet.ilcompiler/8.0.0-preview.6.23329.7/sdk/libRuntime.ServerGC.a /root/.nuget/packages/runtime.linux-arm64.microsoft.dotnet.ilcompiler/8.0.0-preview.6.23329.7/sdk/libeventpipe-enabled.a /root/.nuget/packages/runtime.linux-arm64.microsoft.dotnet.ilcompiler/8.0.0-preview.6.23329.7/sdk/libstdc++compat.a /root/.nuget/packages/runtime.linux-arm64.microsoft.dotnet.ilcompiler/8.0.0-preview.6.23329.7/framework/libSystem.Native.a /root/.nuget/packages/runtime.linux-arm64.microsoft.dotnet.ilcompiler/8.0.0-preview.6.23329.7/framework/libSystem.IO.Compression.Native.a /root/.nuget/packages/runtime.linux-arm64.microsoft.dotnet.ilcompiler/8.0.0-preview.6.23329.7/framework/libSystem.Net.Security.Native.a /root/.nuget/packages/runtime.linux-arm64.microsoft.dotnet.ilcompiler/8.0.0-preview.6.23329.7/framework/libSystem.Security.Cryptography.Native.OpenSsl.a --sysroot=/crossrootfs/arm64 --target=aarch64-linux-gnu -g '-Wl,-rpath,$ORIGIN' -Wl,--build-id=sha1 -Wl,--as-needed -pthread -ldl -lz -lrt -lm -pie -Wl,-pie -Wl,-z,relro -Wl,-z,now -Wl,--eh-frame-hdr -Wl,--discard-all -Wl,--gc-sections" exited with code 1. [/source/releasesapi.csproj]
The command '/bin/sh -c dotnet publish -a arm64 -o /app -p:CppCompilerAndLinker=clang-16 -p:SysRoot=/crossrootfs/arm64 releasesapi.csproj' returned a non-zero code: 1

Related: Is this guidance about which NuGet packages to work correct?

https://github.com/dotnet/runtime/blob/main/src/coreclr/nativeaot/docs/compiling.md#cross-architecture-compilation

Follow up to #88942

@ghost ghost added the untriaged New issue has not been triaged by the area owner label Jul 16, 2023
@ghost
Copy link

ghost commented Jul 16, 2023

Tagging subscribers to this area: @agocke, @MichalStrehovsky, @jkotas
See info in area-owners.md if you want to be subscribed.

Issue Details

Following the instructions at:

My Dockerfile: https://github.com/richlander/dotnet-docker/blob/dotnet-8-samples/samples/releasesapi-aot/Dockerfile.cross-target

Host: Linux kelowna 6.1.38-1-MANJARO #1 SMP PREEMPT_DYNAMIC Wed Jul 5 23:49:30 UTC 2023 x86_64 GNU/Linux

Docker build: docker build --pull -t app -f Dockerfile.cross-target .

I get this error:

  Generating native code
  ILC: Method '[releasesapi]Program.<Main>$(string[])' will always throw because: Failed to load assembly 'Microsoft.AspNetCore'
  /usr/bin/ld.bfd: unrecognised emulation mode: aarch64linux
  Supported emulations: elf_x86_64 elf32_x86_64 elf_i386 elf_iamcu elf_l1om elf_k1om
clang-16 : error : linker command failed with exit code 1 (use -v to see invocation) [/source/releasesapi.csproj]
/root/.nuget/packages/microsoft.dotnet.ilcompiler/8.0.0-preview.6.23329.7/build/Microsoft.NETCore.Native.targets(364,5): error MSB3073: The command ""clang-16" "obj/Release/net8.0/linux-arm64/native/releasesapi.o" -o "bin/Release/net8.0/linux-arm64/native/releasesapi" -fuse-ld=bfd /root/.nuget/packages/runtime.linux-arm64.microsoft.dotnet.ilcompiler/8.0.0-preview.6.23329.7/sdk/libbootstrapper.a /root/.nuget/packages/runtime.linux-arm64.microsoft.dotnet.ilcompiler/8.0.0-preview.6.23329.7/sdk/libRuntime.ServerGC.a /root/.nuget/packages/runtime.linux-arm64.microsoft.dotnet.ilcompiler/8.0.0-preview.6.23329.7/sdk/libeventpipe-enabled.a /root/.nuget/packages/runtime.linux-arm64.microsoft.dotnet.ilcompiler/8.0.0-preview.6.23329.7/sdk/libstdc++compat.a /root/.nuget/packages/runtime.linux-arm64.microsoft.dotnet.ilcompiler/8.0.0-preview.6.23329.7/framework/libSystem.Native.a /root/.nuget/packages/runtime.linux-arm64.microsoft.dotnet.ilcompiler/8.0.0-preview.6.23329.7/framework/libSystem.IO.Compression.Native.a /root/.nuget/packages/runtime.linux-arm64.microsoft.dotnet.ilcompiler/8.0.0-preview.6.23329.7/framework/libSystem.Net.Security.Native.a /root/.nuget/packages/runtime.linux-arm64.microsoft.dotnet.ilcompiler/8.0.0-preview.6.23329.7/framework/libSystem.Security.Cryptography.Native.OpenSsl.a --sysroot=/crossrootfs/arm64 --target=aarch64-linux-gnu -g '-Wl,-rpath,$ORIGIN' -Wl,--build-id=sha1 -Wl,--as-needed -pthread -ldl -lz -lrt -lm -pie -Wl,-pie -Wl,-z,relro -Wl,-z,now -Wl,--eh-frame-hdr -Wl,--discard-all -Wl,--gc-sections" exited with code 1. [/source/releasesapi.csproj]
The command '/bin/sh -c dotnet publish -a arm64 -o /app -p:CppCompilerAndLinker=clang-16 -p:SysRoot=/crossrootfs/arm64 releasesapi.csproj' returned a non-zero code: 1
Author: richlander
Assignees: -
Labels:

area-NativeAOT-coreclr

Milestone: -

@am11
Copy link
Member

am11 commented Jul 17, 2023

bfd (the default linker on linux) is target specific, while lld (the LLVM linker) is multi-targeting. If, for some reason, we don't have cross aarch64 binutils (tdnf install aarch64-linux-gnu-binutils suggests it's not ported yet?) on a distro, we can use lld linker:

--- a/samples/releasesapi-aot/Dockerfile.cross-target
+++ b/samples/releasesapi-aot/Dockerfile.cross-target
@@ -2,7 +2,6 @@
 # https://github.com/dotnet/dotnet-docker/blob/main/samples/README.md
 FROM mcr.microsoft.com/dotnet-buildtools/prereqs:cbl-mariner-2.0-cross-arm64 AS build
 COPY --from=mcr.microsoft.com/dotnet/sdk:8.0-preview-cbl-mariner2.0 /usr/share/dotnet /usr/share/dotnet
-RUN tdnf install -y build-essential
 RUN ln -s /usr/share/dotnet/dotnet /usr/bin/dotnet
 WORKDIR /source

@@ -12,7 +11,7 @@ RUN dotnet restore -a arm64

 # copy and publish app and libraries
 COPY . .
-RUN dotnet publish -a arm64 -o /app -p:CppCompilerAndLinker=clang-16 -p:SysRoot=/crossrootfs/arm64 releasesapi.csproj
+RUN dotnet publish -a arm64 -o /app -p:CppCompilerAndLinker=clang-16 -p:SysRoot=/crossrootfs/arm64 -p:LinkerFlavor=lld releasesapi.csproj


 RUN rm /app/*.dbg /app/*.Development.json

In my testing, current version of lld produces about 1.2% larger binary than bfd, i.e. an ignorable detail.

@ivanjx
Copy link

ivanjx commented Jul 18, 2023

in my case cross compiling works for arm64 on amd64 with a little hack for objcopy (better suggestion is welcome):

FROM mcr.microsoft.com/dotnet/sdk:8.0.100-preview.6-bookworm-slim-amd64 as build
RUN dpkg --add-architecture arm64 && \
    apt-get update
RUN apt-get install -y \
    clang zlib1g-dev:arm64 \
    binutils-aarch64-linux-gnu gcc-aarch64-linux-gnu
RUN rm /usr/bin/objcopy && \
    ln -s /usr/bin/aarch64-linux-gnu-objcopy /usr/bin/objcopy
COPY ./project /build
WORKDIR /build
RUN dotnet publish \
    -c Release \
    -r linux-arm64 \
    -o /output

i cant use ubuntu images since i cant install zlib1g-dev:arm64 on them so i use debian instead.

@am11
Copy link
Member

am11 commented Jul 18, 2023

There is a simpler -p:ObjCopyName=aarch64-linux-gnu-objcopy workaround for it. :)

@agocke
Copy link
Member

agocke commented Jul 18, 2023

dn-vm/dnvm@c429bf6 demonstrates a working solution.

@richlander
Copy link
Member Author

richlander commented Jul 22, 2023

Here's a Dockerfile that works. Imagine all the apt commands being in the SDK layer.

# Learn about building .NET container images:
# https://github.com/dotnet/dotnet-docker/blob/main/samples/README.md
FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:8.0-preview AS build
ARG TARGETARCH
RUN dpkg --add-architecture arm64
RUN apt update && apt install -y clang zlib1g-dev zlib1g-dev:arm64 \
    gcc-aarch64-linux-gnu
WORKDIR /source

# copy csproj and restore as distinct layers
COPY *.csproj .
RUN dotnet restore -a $TARGETARCH

# copy and publish app and libraries
COPY . .
RUN if [ "$TARGETARCH" = "arm64" ]; then \
        export OBJCOPY=aarch64-linux-gnu-objcopy
    else \
        export OBJCOPY=objcopy
    fi; \
    dotnet publish -a $TARGETARCH --self-contained -o /app releasesapi.csproj -p:ObjCopyName=$OBJCOPY
RUN rm /app/*.dbg /app/*.Development.json


# final stage/image
FROM mcr.microsoft.com/dotnet/runtime-deps:8.0-preview
WORKDIR /app
COPY --from=build /app .
# USER $APP_UID
ENTRYPOINT ["./releasesapi"]

Given this Dockerfile, on x64, both of these commands work:

docker build -t app .
docker build -t app --platform linux/arm64 .

The if statement (as written) won't work for a true multi-platform Dockerfile. We'll need something better.

Is there a way we could move that if statement into the SDK somehow? For example, I could imagine this (the SDK would pick the one that matches $TARGETARCH):

dotnet publish -a $TARGETARCH --self-contained -o /app releasesapi.csproj -p:ObjCopyName-x64=objcopy -p:ObjCopyName-arm64=aarch64-linux-gnu-objcopy

@jkotas
Copy link
Member

jkotas commented Jul 22, 2023

We should be able to get rid of the manually specified ObjCopyName completely in this case. We can either default to lld for cross-compilation; or we can default to the right arch-specific objcopy when cross-compiling. @am11 Do you have a preference?

@richlander
Copy link
Member Author

I tried using lld, but am not sure how to make that work.

This:

dotnet publish -a $TARGETARCH --self-contained -o /app -p:LinkerFlavor=lld releasesapi.csproj

Results in:

clang : error : invalid linker name in argument '-fuse-ld=lld'

@am11
Copy link
Member

am11 commented Jul 22, 2023

The original issue (top post) was CBL mariner image with clang-16 missing cross-platform binutils, for which the fix is to simply use -p:LinkerFlavor=lld.

clang : error : invalid linker name in argument '-fuse-ld=lld'

clang 7 and above support -fuse-ld=lld flag. Seems like now you are using Ubuntu with very older version of clang v6 or below (note: latest release version is 16.0), In which case, neither the linker will produce quality code, nor llvm-objcopy will help. The user should be at least on llvm-10 or above to achieve acceptable results.

@am11
Copy link
Member

am11 commented Jul 22, 2023

We should be able to get rid of the manually specified ObjCopyName completely in this case.

User can have a non-default version of binutils or multiple toolchains installed side-by-side (e.g. different versions of binutils or different toolchain flavors). With cmake, we can easily search for tools belonging to the selected toolchain efficiently, aarch64-linux-gnu-objcopy-12 (instead of aarch64-linux-gnu-objcopy which could be any version).

I think the right way would be to either take dependency on cmake for cross scenarios or implement the probing logic in a shell script to locate the matching linker and objcopy. Also, these days it is much easier to run arch-specific docker image and bypass the complexities of cross toolchain:

# build arm64 image with NativeAOT prereqs:
$ docker build -t dotnet8-aot-arm64-prereqs - <<EOF
FROM arm64v8/ubuntu:latest
RUN apt update && apt install -y clang build-essential libicu-dev curl libz-dev
RUN curl -sSL https://dot.net/v1/dotnet-install.sh | bash /dev/stdin -quality preview -channel 8.0
ENV PATH=\${PATH}:/root/.dotnet

# warmup the image with NativeAOT and R2R packs
RUN dotnet new console -o /tmp/warmup
RUN dotnet publish -p:PublishAot=true /tmp/warmup
RUN dotnet publish -p:PublishReadyToRun=true /tmp/warmup
EOF


# usage:
$ docker run --rm -v$(pwd):/myproject dotnet8-aot-arm64-prereqs \
    dotnet publish -p:PublishAot=true /myproject

@richlander
Copy link
Member Author

richlander commented Jul 22, 2023

I'm using the clang in latest Debian.

$ clang --version
Debian clang version 14.0.6
Target: x86_64-pc-linux-gnu
Thread model: posix
InstalledDir: /usr/bin
$ cat /etc/os-release | head -n 1
PRETTY_NAME="Debian GNU/Linux 12 (bookworm)"

My goal here is to enable users to create a multi-platform-friendly Dockerfile. We do that with the SDK today.

Context: https://devblogs.microsoft.com/dotnet/improving-multiplatform-container-support/

It seems like we are really close here to enabling the same experience for native AOT.

I like the warmup you are doing. Very nice.

@richlander
Copy link
Member Author

OK. I figured the most critical issue. lld wasn't installed. I installed the lld package and the issue about '-fuse-ld=lld' is now gone. Cross-compiling is still a challenge, however. Still working on that.

@richlander
Copy link
Member Author

richlander commented Jul 22, 2023

This works. Again, imagine that the two RUN lines are not there. Then, users have a pretty nice experience for cross-compiling like we offer for the other .NET app types.

This is very similar to what @agocke is doing in his working example.

FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:8.0-preview AS build
ARG TARGETARCH
RUN dpkg --add-architecture arm64
RUN apt update && apt install -y clang zlib1g-dev zlib1g-dev:arm64 gcc-aarch64-linux-gnu llvm
WORKDIR /source

# copy csproj and restore as distinct layers
COPY *.csproj .
RUN dotnet restore -a $TARGETARCH

# copy and publish app and libraries
COPY . .
RUN dotnet publish -a $TARGETARCH --self-contained -o /app releasesapi.csproj
RUN rm /app/*.dbg /app/*.Development.json


# final stage/image
FROM mcr.microsoft.com/dotnet/runtime-deps:8.0-preview
WORKDIR /app
COPY --from=build /app .
USER $APP_UID
ENTRYPOINT ["./releasesapi"]

@richlander
Copy link
Member Author

I'm thinking -- given the size of these dependencies -- that we could offer three sdk variants:

  • sdk -- for CoreCLR apps
  • aot-sdk -- for native AOT apps
  • aot-cross-sdk -- for cross compiling native AOT apps (enables the Dockerfile above)

@am11
Copy link
Member

am11 commented Jul 23, 2023

I'm using the clang in latest Debian.

Ok, it is different than:

My Dockerfile: https://github.com/richlander/dotnet-docker/blob/dotnet-8-samples/samples/releasesapi-aot/Dockerfile.cross-target

that one is using CBL-mariner with llvm toolchain installed, incl. lld and llvm-objcopy (without the version suffix), which is why #88971 (comment) applies.

Since the official .NET CI in .NET 8 is using the same CBL-based images with cross-toolchain installed, to validate PR changes and create official builds; I think it would be a good idea for end-user to consume the same base images. If a user, who prefers the $other distro, wants to build the cross-toolchain, they are free to build a custom image.

Alternatively, IMO the best way in this day and age is to not engage with cross-compilation at all, and instead use cross-arch docker image. On win, macOS, and some linux distros, we don't need to install extra packages to enable cross-arch'ness anymore; just install docker and all the dependencies get in place. Using this approach will make sure our project file and/or command-line arguments are no more unusual than -p:PublishAot=true bit and we get the binaries for target architecture.

@ivanjx
Copy link

ivanjx commented Jul 23, 2023

@am11 dotnet sdk does not support qemu thats why cross compilation (amd64, arm64) is needed for now.

@am11
Copy link
Member

am11 commented Jul 23, 2023

@ivanjx, .NET 8 SDK works with QEMU just fine. Make sure to disable the write-xor-execute feature; DOTNET_EnableWriteXorExecute=0.

  1. Install latest Docker on x64 machine. (I updated Docker Desktop on win-x64 to latest version today)

  2. Create an empty directory containing a Dockerfile with these contents:

    FROM --platform=$BUILDPLATFORM arm64v8/ubuntu:latest
    
    RUN apt update && apt install -y clang build-essential libicu-dev curl libz-dev
    RUN curl -sSL https://dot.net/v1/dotnet-install.sh | bash /dev/stdin -quality preview -channel 8.0
    
    ENV DOTNET_EnableWriteXorExecute=0
    ENV PATH="${PATH}:/root/.dotnet"
    
    # warmup the image with NativeAOT and R2R packs
    RUN dotnet new console -o /tmp/warmup
    RUN dotnet publish -p:PublishAot=true /tmp/warmup
    RUN dotnet publish -p:PublishReadyToRun=true /tmp/warmup
  3. Build the arm64 image for reuse:

    $ docker build -t dotnet8-aot-arm64-prereqs .
  4. Use it:

    # in powershell syntax on win-x64
    
    # create a webapi app
    $ docker run --platform=linux/arm64/v8 --rm -v ${pwd}/api1:/api1 dotnet8-aot-arm64-prereqs `
          dotnet new api --aot -o /api1
    # note: starting with net8.0-rc1 (currently in daily build), instead use: dotnet new webapiaot -n api1
    
    # publish the webapi app
    $ docker run --platform=linux/arm64/v8 --rm -v ${pwd}/api1:/api1 dotnet8-aot-arm64-prereqs `
          dotnet publish -c Release /api1 -o /api1/dist
    
    # webapi arm64 binaries are available at:    ./api1/dist

emulation is usually slower than cross, but on CI build machine, it doesn't matter that much (as long as the builds are succeeding).

@am11
Copy link
Member

am11 commented Jul 23, 2023

https://github.com/dotnet/core/blob/main/release-notes/8.0/supported-os.md#qemu

Right, so evidentially docs could use a lighter tone and avoid calling stuff "unsupported" when runtime (the thing which basically matters most for platform support) does not enforce any restrictions for emulators, and neither does the SDK specifically disallows them. If note about unsupported stuff is really necessary in the docs, it should read something like: "QEMU is not officially supported, use it at your own risk"

@MichalPetryka
Copy link
Contributor

https://github.com/dotnet/core/blob/main/release-notes/8.0/supported-os.md#qemu

Right, so evidentially docs could use a lighter tone and avoid calling stuff "unsupported" when runtime (the thing which basically matters most for platform support) does not enforce any restrictions for emulators, and neither does the SDK specifically disallows them. If note about unsupported stuff is really necessary in the docs, it should read something like: "QEMU is not officially supported, use it at your own risk"

.NET did not work on QEMU at all last time that I've tried.

@richlander
Copy link
Member Author

I will go back to the cbl-mariner approach next. As is likely obvious, the Debian and Ubuntu latest approaches will not produce equivalent results to the cbl-mariner one. However, the cbl-mariner one isn't a great default option.

@richlander
Copy link
Member Author

richlander commented Jul 24, 2023

FYI: I believe I have everything I need working now. Thanks MUCH for the help. It was tremendously useful. The final results were simpler than where I started, which is great. I also learned a fair bit.

I'll be updating my samples PR in the next 24 hours or so with my results.

dotnet/dotnet-docker#4742

@ghost ghost locked as resolved and limited conversation to collaborators Aug 23, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

No branches or pull requests

6 participants