Mehdi Akiki
Rust Failure Atlas / Cargo and dependencies

RFA-576 · Case file with fixtures · Case 548 of 694 · Compiler evidence

A Rust Binary Crate Needs a main Entrypoint

Crate type determines the required entry contract. Keep main thin, put behaviour in testable functions or a library, and verify target configuration.

Reviewed
Rust
Rust 1.98.1, edition 2024
Targets
executable targets
Profiles
dev, release, test

Direct answer

What this Rust failure means

Why it happens
Useful library-like functions were compiled under an executable target contract that requires a language-recognised process entrypoint.
First discriminating check
Confirm the Cargo or rustc crate type and active cfg, then add a thin main or move reusable logic to the intended library target.

A source file can contain useful functions and still not be a complete executable. When rustc treats the file as a binary crate, it needs a crate-level main entrypoint. The failing fixture defines run_worker but never defines how process execution begins, so rustc emits E0601.

Crate type changes the compilation contract

The same helper would be perfectly valid inside a library. Libraries expose items to callers; executables need a starting point chosen by the Rust runtime. E0601 therefore often means the target was classified differently from what the author expected.

The official E0601 page states the immediate repair. I also inspect why rustc is building a binary. A direct rustc file.rs command defaults to an executable. Cargo recognises src/main.rs and files under src/bin as binary targets, while src/lib.rs defines a library target. Explicit [[bin]] and [lib] sections can change paths.

Renaming a file or copying an example into src/bin can therefore expose E0601 without any change to its helper functions.

A thin main keeps the program testable

The repaired fixture adds main and delegates to run_worker. Real applications benefit from the same shape, though run will usually accept parsed configuration and return a result.

I keep process-only responsibilities near main: read arguments and environment, initialise tracing, build runtime resources, call application logic, and map the final outcome to an exit code. Business rules stay in ordinary functions or the package's library crate, where unit and integration tests can call them without launching another process.

This structure also helps multiple binaries share one engine. A server, migration tool, and administrative command can each have a small entrypoint while importing common behaviour from the library target.

main is found at the crate level

A function named main inside a nested module does not automatically become the process entry. The required function belongs at the crate root unless language-supported attributes and target environments define another entry model.

The Reference section on main functions documents the accepted role. E0601 is different from E0580, where a main exists but has an invalid signature. I check both presence and signature rather than assuming any function named main is sufficient.

Some targets intentionally use #![no_main], such as embedded programs, kernels, or custom runtimes. That opts out of the normal Rust entry contract and transfers substantial responsibility to the program. It is not a general way to silence E0601. The custom symbol, ABI, startup state, panic behaviour, and linker configuration all need a target-specific proof.

Cargo configuration is part of the diagnosis

In a large workspace I run cargo metadata or inspect the manifest targets to see which package and binary Cargo selected. Feature flags and examples can create additional target combinations. An error path may point at generated output, but the owning manifest still determines why that output is an executable.

Tests have their own harness behaviour. The test harness can provide an entrypoint, while disabling it changes the contract. Benchmarks and examples similarly have target-specific expectations. I avoid copying crate attributes between target kinds without understanding them.

For command-line applications, I add at least one end-to-end smoke test for argument parsing and exit status. Unit tests of run cannot prove the binary target is wired, named, and packaged correctly.

Startup failure is part of the interface

I also decide how initialisation errors reach the operator. Returning a supported result from main, or handling the result in a thin wrapper, should produce a useful message and non-zero status without dumping secrets. Panicking may be acceptable for a small internal tool but is usually a poor configuration-error interface. Keeping this policy at the entry boundary lets the reusable engine return structured errors without knowing about terminals or process exit codes.

For services, graceful shutdown belongs to the orchestration called by main, not to unrelated domain functions. A smoke test can start the real executable with a harmless configuration, observe readiness, send its shutdown signal, and verify a clean exit. This proves much more of the actual entry contract than merely compiling the function.

My E0601 checklist

  • Is rustc compiling this source as a binary, example, test, or library?
  • Which Cargo target declaration selected the file?
  • Does a crate-level main exist under the active cfg values?
  • Is application logic separated from process bootstrapping?
  • Did a file move or generated target change its crate role?
  • Is no_main genuinely required by a documented runtime contract?
  • Do all supported feature combinations retain an entrypoint?
  • Does a packaged-binary smoke test cover startup and exit behaviour?

The core principle is that useful code and an executable entry contract are different things. I fix E0601 by making the intended crate type explicit, then keep main small enough that the important engineering remains reusable and directly testable.