Docs menu

Customizing the build

How to shape the .NET build on Primcloud: MSBuild targets, custom publish flags, native dependencies, and Debian packages via project.toml.

On this page

Most .NET apps deploy on Primcloud with no custom build steps. The build runs dotnet publish against your solution or project and that's it. When you need more, .NET's own extension points do the work: MSBuild targets, project.toml config, and Debian package installs for OS-level libraries.

Build hooks#

Custom pre- and post-publish work goes through MSBuild targets in your project file. These run during dotnet publish as part of the normal build graph, so local and Primcloud builds stay identical.

Run a command before publish:

MyApp.csproj
<Target Name="RunPreBuildScript" BeforeTargets="Publish">
  <Exec Command="./scripts/pre-publish.sh" />
</Target>

Run a command after publish:

MyApp.csproj
<Target Name="StampBuildInfo" AfterTargets="Publish">
  <WriteLinesToFile File="$(PublishDir)/BUILD_SHA" Lines="$(BUILD_SHA)" Overwrite="true" />
</Target>

$(PublishDir) resolves to bin/publish on Primcloud (see .NET on Primcloud → Compile phase). Environment variables set on the environment are available to <Exec> via $(VarName).

Custom install steps#

To adjust the publish command itself (MSBuild configuration, verbosity, which solution to build), use the two config surfaces the build reads:

Via project.toml at the repo root:

project.toml
[_]
schema-version = "0.2"

[com.heroku.buildpacks.dotnet]
solution_file = "MyWebApp.sln"

[com.heroku.buildpacks.dotnet.msbuild]
configuration = "Release"
verbosity = "normal"

Via environment variables on the environment:

  • SOLUTION_FILE: solution file to build (overrides project.toml).
  • BUILD_CONFIGURATION: MSBuild configuration; defaults to Release.
  • MSBUILD_VERBOSITY_LEVEL: MSBuild verbosity; defaults to minimal.

If you need to inject additional MSBuild properties, set them directly in your project file or a Directory.Build.props. Primcloud doesn't pass extra flags into dotnet publish beyond what's above.

Native dependencies#

There are two shapes of "native" your .NET app can rely on:

Managed wrappers around bundled natives. Packages like SkiaSharp or Microsoft.Data.SqlClient ship platform-specific native binaries inside the NuGet package under runtimes/<rid>/native/. These are restored as part of dotnet restore and included in the published output for the target RID (linux-x64 or linux-arm64). No extra setup required; they work at runtime.

OS-level shared libraries your code calls via P/Invoke, or that a native NuGet dependency links against: libgdiplus, libssl, libmariadb, and so on. These are provided by the operating system, not restored via NuGet. Declare them in project.toml, covered in the next section.

Debian packages#

When your app needs OS-level libraries or CLI tools that aren't in Primcloud's base image, declare them in a project.toml at the repo root. Primcloud's build installs them automatically when it finds the [com.heroku.buildpacks.deb-packages] table.

Declare packages to install:

project.toml
[_]
schema-version = "0.2"

[com.heroku.buildpacks.deb-packages]
install = [
  "libgdiplus",
  "libmariadb-dev",
]

Each entry is a Debian package name from the Ubuntu 24.04 archive. Packages are installed into their own layer and their libraries added to LD_LIBRARY_PATH for both the build and runtime phases, so P/Invoke calls at runtime resolve without extra config.

Advanced options (rarely needed):

  • Skip transitive dependencies: { name = "some-package", skip_dependencies = true }
  • Force reinstall: { name = "some-package", force = true }
  • Install from a URL: download = ["https://example.com/package-1.2.3.deb"]
  • Add a custom Debian repository: a [[com.heroku.buildpacks.deb-packages.sources]] table with uri, suites, components, arch, and signed_by (the source key as an ASCII-armored PGP block).

Note

If you're migrating from Heroku Classic and have an Aptfile at the repo root, the build does not read it. It detects the file and prints a migration warning, then exits without installing anything. Move your package list into project.toml under the schema above.