Here's the solution I came up with. This is based on code from com.intellij.openapi.roots.ui.configuration.classpath.ChangeLibraryLevelActionBase.doCopy(LibraryEx library)
private void copyGlobalLibraryToProject( Project project, String libraryName ) {
LibraryTable globalTable = LibraryTablesRegistrar.getInstance().getLibraryTable();
final LibraryEx sourceLibrary = Objects.requireNonNull( (LibraryEx )globalTable.getLibraryByName( libraryName ) );
LibraryTable libraryTable = LibraryTablesRegistrar.getInstance().getLibraryTable(project);
final LibraryTable.ModifiableModel modifiableModel = libraryTable.getModifiableModel();
final Set<File> fileToCopy = new LinkedHashSet<>();
final Map<String, String> copiedFiles = new HashMap<>();
for (OrderRootType type : OrderRootType.getAllTypes()) {
for (VirtualFile root : sourceLibrary.getFiles(type)) {
if (root.isInLocalFileSystem() || root.getFileSystem() instanceof ArchiveFileSystem ) {
fileToCopy.add( VfsUtilCore.virtualToIoFile( VfsUtil.getLocalFile(root)));
}
}
}
fileToCopy.forEach( from -> copiedFiles.put( FileUtil.toSystemIndependentName(from.getAbsolutePath()), FileUtil.toSystemIndependentName(from.getAbsolutePath()) ) );
final Library copied = modifiableModel.createLibrary( libraryName, sourceLibrary.getKind());
final LibraryEx.ModifiableModelEx model = (LibraryEx.ModifiableModelEx)copied.getModifiableModel();
LibraryEditingUtil.copyLibrary(sourceLibrary, copiedFiles, model);
WriteAction.run(() -> {
model.commit();
modifiableModel.commit();
});
}