I am afraid your project is relying on a behavior that is not guaranteed. You’ll need to make maybePreemptible true to generate a PLT.
LLD had some inconsistency and I’ve refactored the code for version 20
The purpose of Symbol::includeInDynsym was somewhat ambiguous, as it was used both to determine if a symbol should be exported to .dynsym and to conservatively suppress transformations in other contexts like MarkLive and ICF. LLD 20 clarifies this by introducing Symbol::isExported specifically for indicating whether a defined symbol should be exported. All previous uses of Symbol::includeInDynsym have been updated to use Symbol::isExported instead. The old confusing Symbol::exportDynamic has been removed.
A special case within Symbol::includeInDynsym checked for isUndefWeak() && ctx.arg.noDynamicLinker. (This could be generalized to isUndefined() && ctx.arg.noDynamicLinker, as non-weak undefined symbols led to errors.) This condition ensures that undefined symbols are not included in .dynsym for statically linked ET_DYN executables (created with clang -static-pie).
This condition has been generalized in LLD 20 to (ctx.arg.shared || !ctx.sharedFiles.empty()) && (sym->isUndefined() || sym->isExported). This means undefined symbols are excluded from .dynsym in both ld.lld -pie a.o and ld.lld -pie --no-dynamic-linker a.o, but not ld.lld -pie a.o b.so. This change brings LLD’s behavior more in line with GNU ld.
Symbol::isPreemptible, indicating whether a symbol could be bound to another component, was calculated before relocation scanning and, in LLD 19, also during Identical Code Folding (ICF). In LLD 20, the ICF-related calculation has been moved to the symbol versioning parsing stage.
Looking around the tubes a little I see that there is a consensus that the gnu ld behaviour is somewhat inconsistent.
However, there are many cases when that behaviour (old llvm behaviour) is desired, mostly around using LD_PRELOAD to override weak symbols in pie executables.