[Pitch] Package Manager: Statically link Swift runtime libraries by default on supported platforms

Yep, ICU has been dropped from the standard library, and I've noticed @kubamracek recently added a hermetic-seal-at-link flag which enables more aggressive LTO-based dead-stripping. Dead-stripping is currently a big weakness in Swift, because it's very high-level so there's a lot of metadata (e.g. witness tables for protocol conformances) floating around. It's difficult for the compiler to know whether those things are really used or not, so they can't be stripped.

The tests indicate that this feature isn't quite ready for non-Apple platforms yet, but once it is, hopefully we'll be able to get those binary sizes right down. That's nice for package dependencies, which are already statically linked, but the standard library is designed to make heavy use of generics so I expect we'll also see big benefits there.

LTO isn't a thing for dynamic libraries (AFAIK), because... you know, they're linked at runtime by the dynamic linker. I think we'd really want to take advantage of this everywhere that we can, so defaulting to static linking the runtime libs is a good move. +1.

5 Likes

+1 from me as well, this makes total sense for Linux environments. Additionally, the deployment story for server side swift applications becomes way better without us having to version the environment.

It sounds like the goal here is for programmers of container-deployed executables (chiefly server programmers) to be able to get a simple swift build to build their binaries with --static-swift-stdlib without having to explicitly specify that every time. I assume swift build can pick up preferred build-configuration options from the package it's building. Should there just be a swift package init --type X that defaults to building things in the best way for a container-deployed executable, which would include --static-swift-stdlib?

There is no place today to capture such preference. Are you implying that instead of changing the default behavior we should add a new "build settings" section of sorts to Package.swift to capture preferences of this kind and it will be the package author responsibility to set it?

Hmm. I assume it must've been a philosophical decision to not have that. I find that pretty surprising; it certainly seems to assume that there's one standard way that executables should be built on a platform, which puts us in the unfortunate position of having to pick that globally. Dynamic linking is a more reasonable default for a lot of use cases even on Linux; it's mostly containerization that prefers statically linking every non-system dependency. I understand that server development, and thus containers, is the central driver of our current adoption on Linux; server developers shouldn't be required to pass a specific command-line flag to swift build as the only way to get acceptable output. But this proposal seems to be just flipping a switch such that, in a year or two, we'll be forcing a different set of users to pass a different command-line flag to swift build as the only way to get acceptable output.

It sure seems to me like the right fix is to recognize that there's something we're failing to express in packages that the build system ought to be aware of, one way or another. If we philosophically don't want a build-settings section, maybe the best approach is to distinguish between kinds of executables, so that executables destined for containers are a different basic kind of product. The build system could then default to different build settings for that kind of product, like statically linking every non-system dependency.

19 Likes

I do not know why such was not added originally. cc @ddunbar

This focus of the proposal is about what would be considered a sensible default. Even if we decide to add some sort of build settings section to the manifest to capture the user's preference (which seems like a good but additive suggestion), SwiftPM would still need to have a default behavior when such are not specified. Right now, that default behavior across all platforms is dynamic linking [of the runtime libraries], but one could argue the default should be different across platforms, reflecting how the Swift runtime is distributed on those platforms.

Given that no Linux distribution ships the Swift runtime libraries, nor is there a stable ABI for Swift on Linux, the proposal argues that the sensible default on Linux should be static linking [of the runtime libraries]. If and when a Linux distribution ships with a runtime, the toolchain on that distribution could change this default to reflect that, or we could decide to further refine the behavior.

9 Likes

That would also most definitely be nice, but is as you point somewhat orthogonal to this pitch which is an easy “bridging” win even if pursuing such a new setting, to decrease friction in the short term (with no major long term tech debt really). Given the current state server side on Linux, there are a number of already fairly strong reasons to flip the default for Linux already mentioned (for the runtime libraries).

1 Like

My position is that static linking is a sensible default only for executables destined for containers. The standard platform behavior for non-containerized executables on Linux and the BSDs, as defined by platform maintainers through their standard package systems, is to dynamically link your dependencies. You don't need dependencies to have a stable ABI or be distributed with the OS for that to be useful.

I understand that containerized server development is driving most of our usage on Linux right now, and I definitely sympathize with wanting to give those developers the immediate win of swift build just building correctly out of the box without needing any extra command-line flags. However, we've also talked about a number of other build-time configurations that probably ought to be different for containerized server executables, such as linking in a non-standard crash handler / backtracer, or even someday building with crash unwinding enabled in Swift. I'm sure an actual server developer could identify any number of other things along these lines.

If we want those things to also just work out of the box, we're going to have two alternatives:

  • We can continue to recognize only one kind of executable. In this case, we will need to change the defaults to be ever more specific to whatever we've decided to bless as the default use-case for the platform. People who want to build differently will have to override those defaults. As we add features to the system, they may need to override more and more.

  • We can recognize different kinds of executables. As we add features to the system, we can make intelligent decisions about the right defaults for different kinds of executables. Any particular version of swiftpm will make the best decision for what it supports.

I don't think the latter entails having to make a million design decisions immediately. We just pick a name for it, and initially it means exactly the same as executable except using static linking of non-system dependencies. As we grow the system, it can evolve.

I guess executable would mean something like "platform CLI executable". IIUC, Windows would benefit if we also distinguished GUI programs from CLI, because we ought to be emitting winmain instead of main. That would be another example of a standard kind of executable; I don't think it's mandatory for the initial proposal.

14 Likes

Recognizing different kinds of executable products is a great direction and a design space the SwiftPM team has been exploring in depth as part of the plugins effort. Given the breadth of type of products that could fall into this category (CLI, web-service, daemon, windows GUI application, Linux GUI application, etc) we believe the most fitting design is plugin based API that would allow product specific type safe configuration. Said differently, having all these product types hard coded into SwiftPM would not be scalable, and the SwiftPM plugin mechanism was designed to provide infrastructure for such feature.

Orthogonal to this, there is the question of what is the sensible default for platforms that do not ship with the Swift runtime libraries, and the Linux platform is one key example. Right now the default is dynamic linking and the proposal is focused on changing this default to static. IIUC you seem to suggest that dynamic is the sensible default:

Taking a concrete non-server/non-containerized example, how would a user distribute a Swift based CLI tool (an executable) on Linux today? The Swift project does not provide a Linux native packages that contains the runtime libraries so is there is no way to express a dependency on the Swift runtime libraries "through their standard package systems". As such the only choices I am aware of are to either:

  1. use static linking
  2. package these libraries along side the executable (tarball / zip file etc)

Are you aware of other options? If not, I would claim that option 1 is obviously the more sensible of the options, and as such the proposal is to make it the default behavior on Linux. If and when Linux distributions start shipping Swift runtime libraries and/or there is a way to express dependency on version-specific Swift runtime libraries through Linux native package systems we can revisit this default.

6 Likes

I feel like this is generalizing the feature to the point that it has to be solved in a way that no longer addresses the original problem of wanting a correct build "out of the box". It's common for IDEs and build systems to make coarse-grained distinctions like console vs. GUI applications. Supporting 3-4 such configurations covering all the major kinds of executables (console, GUI, containerized, maybe embedded) is not unscalable.

Given that this is something we want to do, I don't see why we should be making irrevocable decisions based on not having done it yet. And yes, the typical platform answer for this would be to make a package for your program that depends on a Swift package.

Let's look at it from another angle. If we agree that we want to have fine-grained distinctions between kinds of executable, whether by builtin logic or by plugins, then the question is what executable will mean in that world. My proposal is that we should pick something arbitrary but consistent for it, probably the thing with minimal assumptions, like a platform console application. Your proposal is essentially that we should pick whatever we decide is the favored use case for the target, and that containerized executables should be favored outside of Apple platforms. To me, that seems like it will lead to a world in which it is essentially wrong to ever use executable instead of a fine-grained alternative, at least if you care about portability.

2 Likes

More precisely, I believe this is the kind of decision that distro maintainers make. Dynamic linking is important for getting security bug-fixes out to desktop clients. A world in which every package had to be recompiled whenever a new C runtime was released by the gcc team would be a world in which most packages stay broken because they are not actively maintained.

4 Likes

Note that linux distributions provide a way to install just the runtime for a language (e.g. libstdc++) and even have the ability to have multiple parallel versions (with SO versioning) (e.g. exherbo's slots for libstdc++, libgomp, etc). The packages then depend on the runtime at the particular version that they need.

2 Likes

The point of the proposal is to have a sensible default until all these other ideas materialize, which could take a while. Incidentally, I am involved in the effort to create native Linux packages for Swift so know first hand it will take time to get this to the point where Swift programs can express such runtime dependency. For example, Swift's lack of ABI on Linux as well as it's evolution velocity and versioning scheme (introducing major features in semantically minor releases) make it harder to do and we would need to find a way to install many versions of Swift side-by-side on the same system which conflicts with some of the assumptions the Swift toolchain makes. This is not to say we should not work to solve these problems, but to emphasize that it will take time to get there.

From where I stand, the current default is hurting Swift users on Linux and the adoption of Swift on Linux. In that context, its worth mentioning that go and rust, both pretty popular on Linux, choose static linking based approaches by default. Further, in no way is changing the default irrevocable - we have one default today and this proposal suggests we change it. We can change it again if and when all the pre-requisites for dynamic linking are in place on a critical mass of Linux distributions, or when Linux distributions start shipping Swift they can have a different default on their version of the toolchain.

7 Likes

I think for Linux there are two perspectives that it might be useful to consider:

First, that of a user (who might not be familiar with Swift, or necessarily a developer at all) wanting to use some Swift software that is not packaged for their distro. They'd install a Swift toolchain, download the code, run a build command and copy the binary somewhere in their PATH.

Second, someone looking to package a piece of Swift software for some distro, which involves writing some variation of build script and metadata/dependency declaration.

The packager would surely want to declare a dependency on some Swift runtime package, and dynamically link against it, to reap all the benefits provided by that.

The user, on the other hand, would probably be best served by static linking, as that means they don't really need to know anything about Swift and its runtime. They can copy the binary to different machines, and change or remove the Swift toolchain from the machine they built on without consequence.

I would assume that more people build and use a non-package-managed executable of any particular software than there are people creating a package for the same. Thus I think a default that serves the user rather than the packager (who should also be more familiar with the concept of static/dynamic linking and thus able to make their own decision) is the right choice.

I'd also argue that static linking is a "safer" default more in keeping with progressive disclosure. The statically linked binary might waste disk space and ram, but the dynamically linked when mishandled can become completely unusable.

1 Like

Go literally doesn't support dynamic linking to other Go code at all; it's only capable of dynamically linking to C. Rust's support for dynamic linking is a bit better, but I get the impression that it's pretty marginal.

We absolutely wouldn’t change it back. It would just break the build process for every containerized executable that people make from this point on. This proposal argues that the proposed change is okay because nobody who matters actually wants dynamic linking, but the reverse would clearly not be true. That is why I'm saying that this proposal is essentially codifying that Swift for Linux is meant for containerized executables.

5 Likes

It codifies that Swift for Linux via end-user invocation of spm is primarily meant for containerized executables. As long as there’s an easy way for distro maintainers and DIYers to override the default and link the target and all its dependencies dynamically, that may be the right default.

2 Likes

I don't think this is true. I think it's saying that naively typing swift build on Linux is meant for static binaries.

If distro maintainers have opinions about how to distribute their source, SwiftPM should absolutely support those use-cases. It should without a doubt be possible to say swift build --dynamic-swift-runtime or whatever the flag ends up being. The question is not whether this should be possible, merely whether it should be the default. It seems strange to me to say that our default should be optimised for a use-case we do not have and have no timeline to reach, while it should be pessimised for 100% of the use-cases we do have, and will continue to have.

11 Likes

I think it would be feasible to change the default in the future by tying it to the tools-version, so that existing packages would get the old default until they upgrade.

2 Likes

I think the central disagreement here is actually about how much complexity and customization is necessary to support different patterns of building executables.

I believe we all agree that statically linking the Swift libraries is just one thing among many that server deployments will want to do differently by default. And I believe we all agree that it would be reasonable for server packages to say, hey, this is a server package, you should build it with those different rules. The difference is that:

  • I am suggesting that it would be reasonable for swiftpm to be taught to recognize that innately. The package can say it’s a server, and swiftpm will know what that means, and it will default to the best rules it knows for building a server on the target platform.

  • The counter-argument seems to be that this is not reasonable for swiftpm to do innately. This kind of customization must be done via a complex, yet-to-be-designed plugin mechanism. So in the short term, we will still teach swiftpm all of those best rules for building a server, and we will make them the default for all executables except on Darwin.

Given that containers can easily handle complex configuration options, I don’t really see why the defaults should cater to them. The defaults should cater to manual invocation and best practices for those environments.

That being said, there should almost certainly be official recommendations on how to configure SPM when containerizing.