What social or technical factors led to the rise of the CIY mentality?
The root cause is pretty obviously the technical reason: Binary-portability is harder than source-portability. Outside of distro packages, most Free software is still only available in source form because that's vastly more convenient for the author(s)/maintainer(s).
Until Linux distros started packaging most things that average people would want to use, your only option was to get the source and compile it for your own system. Commercial Unix vendors typically didn't include stuff that almost everyone wanted (e.g. a nice shell like GNU bash
or similar), just their own implementation of sh
and/or csh
, so you needed to build stuff yourself if you (as a sys-admin) wanted to provide a nice Unix environment to your users for interactive use.
The situation now, with most people being the only admin and only user of the machine sitting on their desktop, is vastly different from the traditional Unix model. A sysadmin maintained the software on the central system, and on everyone's desktop. (Often by having people's workstations just NFS-mount /opt
and /usr/local/
from the central server, and installing stuff there.)
Before things like .NET and Java, true binary-portability across different CPU architectures was impossible. Unix culture evolved with source-portability as the default for this reason, with little effort to even try to enable binary-portability until recent Linux efforts like LSB. For example, POSIX (the major Unix standard) only attempts to standardize source-portability, even in recent versions.
Related cultural factor: Early commercial AT&T Unix came with source code (on tapes). You didn't have to build the system from source, it was just there in case you wanted to see how something really worked when the docs weren't enough.
Wikipedia says:
"The Unix policy of extensive on-line documentation and (for many years) ready access to all system source code raised programmer expectations, and contributed to the 1983 launch of the free software movement."
I'm not sure what motivated this decision, since giving customers access to the source code of commercial software is unheard-of these days. There are clearly some early cultural biases in this direction, but perhaps that grew out of Unix's roots as a portable OS written mostly in C (not assembly language) that could be compiled for different hardware. I think many earlier OSes had more of their code written in asm for a specific CPU, so source-level portability was one of early Unix's strengths. (I may be wrong about this; I'm not an expert on early Unix, but Unix and C are related.)
Distribution of software in source form is by far the easiest way to let people adapt it to whatever system they want it to run on. (Either end-users or people packaging it for a Linux distro). If software has already been packaged by/for a distribution, end-users can just use that.
But it's far too much to expect authors of most packages to make binaries for every possible system themselves. Some major projects provide binaries for a few common cases (especially x86/windows where the OS doesn't come with a build environment, and the OS vendor has put a major emphasis on distribution of binary-only installers).
Getting a piece of software to run on a different system from the one the author used might even require some small changes, which are easy with source. A small one-off program that someone wrote to scratch their own itch probably has never been tested on most obscure systems. Having the source makes it possible to make such changes. The original author might have overlooked something, or intentionally wrote less portable code because it saved a lot of time. Even major packages like Info-ZIP didn't have testers on every platform right away, and needed people to send in their portability patches as problems were discovered.
(There are other kinds of source-level portability issues that only happen because of differences in build env, and aren't really relevant to the issue here. With Java-style binary portability, auto-tools (autoconf
/auto-make
) and similar things like cmake
wouldn't be needed. And we wouldn't have things like some systems require the inclusion of <netinet/in.h>
instead of <arpa/inet.h>
for ntohl(3)
. (And maybe we wouldn't have ntohl()
or any other byte-order stuff in the first place!)
I develop in .NET languages regularly, so I'm not computer illiterate.
Compile-once, run-anywhere is one of the major goals of .NET and also Java, so it's fair to say that entire languages have been invented in an effort to solve this problem, and your dev experience is with one of them. With .NET, your binary runs on a portable runtime environment (CLR). Java calls it's runtime environment the Java Virtual Machine. You only need to distribute one binary that will work on any system (at least, any system where someone has already implemented a JVM or CLR). You can still have portability problems like, /
vs \
path separators, or how to print, or GUI layout details, of course.
A lot of software is written in languages that are fully compiled into native code. There's no .net
or java bytecode, just native machine-code for the CPU it will run on, stored in a non-portable executable file format. C and C++ are the major examples of this, especially in the Unix world. Obviously this means a binary has to be compiled for a specific CPU architecture.
Library versions are another problem. Libraries can and often do keep the source-level API stable while changing the binary-level ABI. (See Difference between API and ABI.) For example, adding another member to an opaque struct
still changes its size, and requires a recompile with headers for the new library version for any code that allocates space for such a struct, whether it's dynamic (malloc), static (global), or automatic (local on the stack).
Operating systems are also important. A different flavour of Unix for the same CPU architecture might have different binary file formats, a different ABI for making system calls, and different numeric values for constants like fopen(3)
's O_RDONLY
, O_APPEND
, O_TRUNC
.
Note that even a dynamically-linked binary still has some OS-specific startup code that runs before main()
. On Windows, this is crt0
. Unix and Linux have the same thing, where some C-Runtime Startup code is statically linked into every binary. I guess in theory you could design a system where that code was dynamically linked too, and part of libc or the dynamic linker itself, but this is not how things work in practice on any OS I'm aware of. That would only solve the system-call ABI problem, not the problem of numeric values for constants for standard-library functions. (Normally system-calls are made through libc wrapper functions: A normal x86-64 Linux binary for source that uses mmap()
won't include the syscall
instruction, just a call
instruction to the libc wrapper function of the same name.
This is part of why you can't just run i386-FreeBSD binaries on i386-Linux. (For a while, the Linux kernel had a system-call compatibility layer. I think at least one of the BSDs can run Linux binaries, with a similar compat layer, but you of course need Linux libraries.)
If you wanted to distribute binaries, you'd need to make a separate one for every combination of CPU/OS-flavour+version/installed-library-versions.
Back in the '80s / '90s, there were many different types of CPU in common use for Unix systems (MIPS, SPARC, POWER, PA-RISC, m68k, etc.), and many different flavours of Unix (IRIX, SunOS, Solaris, AIX, HP-UX, BSD, etc.).
And that's just Unix systems. Many source packages would also compile and work on other systems, like VAX/VMS, MacOS(m68k and PPC), Amiga, PC/MS-DOS, Atari ST, etc.
There are still many CPU architectures and OSes, although now a large majority of desktops are x86 running one of three major OSes.
So there's already more CPU/OS combinations than you can shake a stick at, even before you start thinking about dependencies on 3rd-party libraries that might be at different versions on different systems. (Anything that's not packaged by the OS vendor would have to be installed by hand.)
Any paths that are compiled into the binary are also system-specific. (This saves RAM and time compared to reading them from a config file at startup). Old-school Unix systems typically had a lot of hand-customized stuff, so there's no way you could make any valid assumptions about what's where.
Distributing binaries was totally infeasible for old-school Unix except for major commercial projects that can afford to build and test on all the major combinations.
Even making binaries for just i386-linux-gnu
and amd64-linux-gnu
is hard. Much time and effort has been spent on things like the Linux Standard Base to make portable binaries possible. Even statically linking binaries doesn't solve everything. (e.g. how should a word-processing program print on a RedHat system vs. a Debian system? How should the install add a user or group for a daemon, and arrange for its startup script to run after every reboot?) Those are not great examples, because recompiling from source doesn't solve them.
Besides all that, back in the day memory was more precious than it is now. Leaving out optional features at compile-time can create smaller binaries (less code size) that also use less memory for their data structures. If a feature required an extra member in every instance of a certain class
or struct
to track something, disabling that feature will shrink the object by 4 bytes (for example), which is nice if it's an object that the program allocates 100k of.
Optional compile-time features these days are most often used to make extra libraries optional. e.g. you can compile ffmpeg
with or without libx264
, libx265
, libvorbis
, and many other libraries for specific video/audio encoders, subtitle handling, etc. etc. More commonly, a lot of things can be compiled with or without libreadline
: if it's available when you run ./configure
, the resulting binary will depend on the library, and provide fancy line-editing when reading from a terminal. If it's not, then the program will use some fall-back support to just read lines from the stdin with fgets()
or something.)
Some projects do still use optional features to leave out un-needed code for performance reasons. e.g. the Linux kernel itself can be built without SMP support (e.g. for an embedded system or an ancient desktop), in which case a lot of the locking is simpler. Or with many other optional features that affect some of the core code, not just leaving out drivers or other hardware features. (Although arch-specific and hardware-specific config options account for a lot of the total source code. See Why is the Linux kernel 15+ million lines of code?)
./configure <options>
, then make and make install. I cut my teeth 30 years ago on AT&T 3B2 servers running AT&T SysV Unix and Gould iron running UTX. Things were much harder back then. Some had the beginnings of theconfigure
process, most you had to manually editmakefile(s)
for your particular system.