Repository: pdm-project/pdm

Issue:
[bug] pdm fails to build whl when use_uv is True
- [x] I have searched the issue tracker and believe that this is not a duplicate.

**Make sure you run commands with `-v` flag before pasting the output.**

## Steps to reproduce

set `pdm config use_uv false`, then make a pdm project, and run `pdm build`, it fails with log
```
pdm.termui: Preparing environment(Isolated mode) for PEP 517 build...
pdm.termui: Error occurs
Traceback (most recent call last):
  File "C:\Users\c.men\AppData\Roaming\pdm\venv\lib\site-packages\pdm\termui.py", line 260, in logging
    yield logger
  File "C:\Users\c.men\AppData\Roaming\pdm\venv\lib\site-packages\pdm\cli\commands\build.py", line 62, in do_build
    sdist_file = SdistBuilder(project.root, project.environment).build(dest)
  File "C:\Users\c.men\AppData\Roaming\pdm\venv\lib\site-packages\pdm\builders\base.py", line 84, in wrapper
    return func(*args, **kwargs)
  File "C:\Users\c.men\AppData\Roaming\pdm\venv\lib\site-packages\pdm\builders\sdist.py", line 14, in build
    self.install(self._requires, shared=True)
  File "C:\Users\c.men\AppData\Roaming\pdm\venv\lib\site-packages\pdm\builders\base.py", line 322, in install
    install_requirements(missing, env)
  File "C:\Users\c.men\AppData\Roaming\pdm\venv\lib\site-packages\pdm\installers\core.py", line 45, in install_requirements
    syncer.synchronize()
  File "C:\Users\c.men\AppData\Roaming\pdm\venv\lib\site-packages\pdm\installers\uv.py", line 39, in synchronize
    builder.build_uv_lock(include_self=self.install_self)
  File "C:\Users\c.men\AppData\Roaming\pdm\venv\lib\site-packages\pdm\formats\uv.py", line 100, in build_uv_lock
    group = req.groups[0]
IndexError: list index out of range
```

## Actual behavior

It fails

## Expected behavior

It successes  just as use_uv is False

## Environment Information

I'm using pdm==2.19.3. I checked the source code, and found that when building whl, the Synchronizer's requirements would be just `pdm-backend`, with `groups=[]`, so `req.groups[0]` would be out of range. In my opinion, the simplest way is to run `uv build` when `use_uv ` is True, it seems `uv build` can recognize the contents in `[tool.pdm.build]` such as `excludes`, `is-purelib`, etc. But `uv build` doesn't support  `--no-sdist`, so if `pdm build --no-sdist`, `tar.gz` should be removed after `uv build` is called.



Discussion:
I have implemented the solution to directly use `uv build` when `use_uv` is true [here](https://github.com/pdm-project/pdm/compare/main...monchin:pdm:main). If it is ok, I'm glad to make a PR.
Can you reproduce it on the main branch?
> Can you reproduce it on the main branch?

Yes, I can reproduce it on 4401ff552bd071537d6b2b017a5a2f5266401d42



Patch:
diff --git a/news/3231.bug.md b/news/3231.bug.md
new file mode 100644
index 0000000000..ed9060d2b7
--- /dev/null
+++ b/news/3231.bug.md
@@ -0,0 +1,1 @@
+Fix the bug that `pdm build` would fail when `use_uv` is true.
\ No newline at end of file
diff --git a/src/pdm/cli/commands/build.py b/src/pdm/cli/commands/build.py
index f4203c4c8f..6c3ad7fb2c 100644
--- a/src/pdm/cli/commands/build.py
+++ b/src/pdm/cli/commands/build.py
@@ -5,6 +5,7 @@
 import shutil
 import tarfile
 import tempfile
+from pathlib import Path
 from typing import Mapping
 
 from pdm.cli.commands.base import BaseCommand
@@ -32,6 +33,7 @@ def do_build(
         wheel: bool = True,
         dest: str = "dist",
         clean: bool = True,
+        verbose: int = 0,
         config_settings: Mapping[str, str] | None = None,
         hooks: HookManager | None = None,
     ) -> None:
@@ -57,29 +59,56 @@ def do_build(
         hooks.try_emit("pre_build", dest=dest, config_settings=config_settings)
         artifacts: list[str] = []
         with project.core.ui.logging("build"):
-            if sdist:
-                project.core.ui.echo("[info]Building sdist...")
-                sdist_file = SdistBuilder(project.root, project.environment).build(dest)
-                project.core.ui.echo(f"[info]Built sdist at {sdist_file}")
-                artifacts.append(sdist_file)
-            if wheel:
+            if not project.config["use_uv"]:
                 if sdist:
-                    project.core.ui.echo("[info]Building wheel from sdist...")
-                    sdist_out = tempfile.mkdtemp(prefix="pdm-build-via-sdist-")
-                    try:
-                        with tarfile.open(sdist_file, "r:gz") as tf:
-                            tf.extractall(sdist_out)
-                            sdist_name = os.path.basename(sdist_file)[: -len(".tar.gz")]
-                            whl = WheelBuilder(os.path.join(sdist_out, sdist_name), project.environment).build(dest)
-                            project.core.ui.echo(f"[info]Built wheel at {whl}")
-                            artifacts.append(whl)
-                    finally:
-                        shutil.rmtree(sdist_out, ignore_errors=True)
-                else:
-                    project.core.ui.echo("[info]Building wheel...")
-                    whl = WheelBuilder(project.root, project.environment).build(dest)
-                    project.core.ui.echo(f"[info]Built wheel at {whl}")
-                    artifacts.append(whl)
+                    project.core.ui.echo("[info]Building sdist...")
+                    sdist_file = SdistBuilder(project.root, project.environment).build(dest)
+                    project.core.ui.echo(f"[info]Built sdist at {sdist_file}")
+                    artifacts.append(sdist_file)
+                if wheel:
+                    if sdist:
+                        project.core.ui.echo("[info]Building wheel from sdist...")
+                        sdist_out = tempfile.mkdtemp(prefix="pdm-build-via-sdist-")
+                        try:
+                            with tarfile.open(sdist_file, "r:gz") as tf:
+                                tf.extractall(sdist_out)
+                                sdist_name = os.path.basename(sdist_file)[: -len(".tar.gz")]
+                                whl = WheelBuilder(os.path.join(sdist_out, sdist_name), project.environment).build(dest)
+                                project.core.ui.echo(f"[info]Built wheel at {whl}")
+                                artifacts.append(whl)
+                        finally:
+                            shutil.rmtree(sdist_out, ignore_errors=True)
+                    else:
+                        project.core.ui.echo("[info]Building wheel...")
+                        whl = WheelBuilder(project.root, project.environment).build(dest)
+                        project.core.ui.echo(f"[info]Built wheel at {whl}")
+                        artifacts.append(whl)
+            else:
+                import subprocess
+
+                dest_dir = Path(dest).absolute()
+
+                uv_build_cmd = [*project.core.uv_cmd, "build", "--out-dir", str(dest_dir)]
+                if verbose == -1:
+                    uv_build_cmd.append("-q")
+                elif verbose > 0:
+                    uv_build_cmd.append(f"-{'v'*verbose}")
+                subprocess.run(uv_build_cmd, check=True)
+
+                # pdm build doesn't include .gitignore, and pdm publish would fail with .gitignore
+                (dest_dir / ".gitignore").unlink(missing_ok=True)
+                for sdist_fp in dest_dir.glob("*.tar.gz"):
+                    if sdist is False:
+                        sdist_fp.unlink(missing_ok=True)
+                    else:
+                        artifacts.append(str(sdist_fp))
+
+                for whl_file in dest_dir.glob("*.whl"):
+                    if wheel is False:
+                        whl_file.unlink(missing_ok=True)
+                    else:
+                        artifacts.append(str(whl_file))
+
         hooks.try_emit("post_build", artifacts=artifacts, config_settings=config_settings)
 
     def add_arguments(self, parser: argparse.ArgumentParser) -> None:
@@ -113,5 +142,6 @@ def handle(self, project: Project, options: argparse.Namespace) -> None:
             wheel=options.wheel,
             dest=options.dest,
             clean=options.clean,
+            verbose=options.verbose,
             hooks=HookManager(project, options.skip),
         )


Test patch:
diff --git a/tests/cli/test_build.py b/tests/cli/test_build.py
index cef49baa93..6c47c00295 100644
--- a/tests/cli/test_build.py
+++ b/tests/cli/test_build.py
@@ -30,6 +30,7 @@ def test_build_command(project, pdm, mocker):
         wheel=True,
         dest=mock.ANY,
         clean=True,
+        verbose=0,
         hooks=mock.ANY,
     )
     assert project.core.state.config_settings == {"a": "1", "b": "2"}