diff --git a/src/poetry/masonry/builders/editable.py b/src/poetry/masonry/builders/editable.py
index 47904bbdab9..58c6c86e07d 100644
--- a/src/poetry/masonry/builders/editable.py
+++ b/src/poetry/masonry/builders/editable.py
@@ -4,6 +4,7 @@
import hashlib
import json
import os
+import shutil
from base64 import urlsafe_b64encode
from pathlib import Path
@@ -211,6 +212,43 @@ def _add_scripts(self) -> list[Path]:
added.append(cmd_script)
+ # Handle file scripts (type = "file" in [tool.poetry.scripts])
+ for name, specification in self._poetry.local_config.get(
+ "scripts", {}
+ ).items():
+ if isinstance(specification, dict) and specification.get("type") == "file":
+ source = specification.get("reference")
+ if not source:
+ self._io.write_error_line(
+ f" - File script {name} is missing"
+ " a \"reference\" field"
+ )
+ continue
+ source_path = self._path / source
+
+ if not source_path.exists():
+ self._io.write_error_line(
+ f" - File script {name} references"
+ f" {source} which does not exist"
+ )
+ continue
+
+ if not source_path.is_file():
+ self._io.write_error_line(
+ f" - File script {name} references"
+ f" {source} which is not a file"
+ )
+ continue
+
+ target = scripts_path.joinpath(name)
+ self._debug(
+ f" - Adding the {name} file script"
+ f" to {scripts_path}"
+ )
+ shutil.copy2(source_path, target)
+ target.chmod(0o755)
+ added.append(target)
+
return added
def _add_dist_info(self, added_files: list[Path]) -> None:
diff --git a/tests/fixtures/file_scripts_dir_ref_project/file_scripts_dir_ref_project/__init__.py b/tests/fixtures/file_scripts_dir_ref_project/file_scripts_dir_ref_project/__init__.py
new file mode 100644
index 00000000000..33f27e6caa8
--- /dev/null
+++ b/tests/fixtures/file_scripts_dir_ref_project/file_scripts_dir_ref_project/__init__.py
@@ -0,0 +1,2 @@
+def main():
+ print("Hello from console entry point")
diff --git a/tests/fixtures/file_scripts_dir_ref_project/pyproject.toml b/tests/fixtures/file_scripts_dir_ref_project/pyproject.toml
new file mode 100644
index 00000000000..6fb44d6fa1e
--- /dev/null
+++ b/tests/fixtures/file_scripts_dir_ref_project/pyproject.toml
@@ -0,0 +1,16 @@
+[tool.poetry]
+name = "file-scripts-dir-ref-project"
+version = "1.0.0"
+description = "A project with a file script referencing a directory."
+authors = ["Test Author "]
+
+[tool.poetry.dependencies]
+python = "^3.7"
+
+[tool.poetry.scripts]
+dir-script = { reference = "bin/some-directory", type = "file" }
+console-entry = "file_scripts_dir_ref_project:main"
+
+[build-system]
+requires = ["poetry-core"]
+build-backend = "poetry.core.masonry.api"
diff --git a/tests/fixtures/file_scripts_missing_ref_project/file_scripts_missing_ref_project/__init__.py b/tests/fixtures/file_scripts_missing_ref_project/file_scripts_missing_ref_project/__init__.py
new file mode 100644
index 00000000000..33f27e6caa8
--- /dev/null
+++ b/tests/fixtures/file_scripts_missing_ref_project/file_scripts_missing_ref_project/__init__.py
@@ -0,0 +1,2 @@
+def main():
+ print("Hello from console entry point")
diff --git a/tests/fixtures/file_scripts_missing_ref_project/pyproject.toml b/tests/fixtures/file_scripts_missing_ref_project/pyproject.toml
new file mode 100644
index 00000000000..457e5ac44b4
--- /dev/null
+++ b/tests/fixtures/file_scripts_missing_ref_project/pyproject.toml
@@ -0,0 +1,16 @@
+[tool.poetry]
+name = "file-scripts-missing-ref-project"
+version = "1.0.0"
+description = "A project with a file script referencing a non-existent file."
+authors = ["Test Author "]
+
+[tool.poetry.dependencies]
+python = "^3.7"
+
+[tool.poetry.scripts]
+missing-script = { reference = "bin/does-not-exist.sh", type = "file" }
+console-entry = "file_scripts_missing_ref_project:main"
+
+[build-system]
+requires = ["poetry-core"]
+build-backend = "poetry.core.masonry.api"
diff --git a/tests/fixtures/file_scripts_no_ref_field_project/file_scripts_no_ref_field_project/__init__.py b/tests/fixtures/file_scripts_no_ref_field_project/file_scripts_no_ref_field_project/__init__.py
new file mode 100644
index 00000000000..33f27e6caa8
--- /dev/null
+++ b/tests/fixtures/file_scripts_no_ref_field_project/file_scripts_no_ref_field_project/__init__.py
@@ -0,0 +1,2 @@
+def main():
+ print("Hello from console entry point")
diff --git a/tests/fixtures/file_scripts_no_ref_field_project/pyproject.toml b/tests/fixtures/file_scripts_no_ref_field_project/pyproject.toml
new file mode 100644
index 00000000000..19da30fade9
--- /dev/null
+++ b/tests/fixtures/file_scripts_no_ref_field_project/pyproject.toml
@@ -0,0 +1,16 @@
+[tool.poetry]
+name = "file-scripts-no-ref-field-project"
+version = "1.0.0"
+description = "A project with a file script missing the reference field."
+authors = ["Test Author "]
+
+[tool.poetry.dependencies]
+python = "^3.7"
+
+[tool.poetry.scripts]
+no-ref-script = { type = "file" }
+console-entry = "file_scripts_no_ref_field_project:main"
+
+[build-system]
+requires = ["poetry-core"]
+build-backend = "poetry.core.masonry.api"
diff --git a/tests/fixtures/file_scripts_project/bin/my-script.sh b/tests/fixtures/file_scripts_project/bin/my-script.sh
new file mode 100755
index 00000000000..2278b5aa584
--- /dev/null
+++ b/tests/fixtures/file_scripts_project/bin/my-script.sh
@@ -0,0 +1,2 @@
+#!/bin/bash
+echo "Hello from file script"
diff --git a/tests/fixtures/file_scripts_project/file_scripts_project/__init__.py b/tests/fixtures/file_scripts_project/file_scripts_project/__init__.py
new file mode 100644
index 00000000000..33f27e6caa8
--- /dev/null
+++ b/tests/fixtures/file_scripts_project/file_scripts_project/__init__.py
@@ -0,0 +1,2 @@
+def main():
+ print("Hello from console entry point")
diff --git a/tests/fixtures/file_scripts_project/pyproject.toml b/tests/fixtures/file_scripts_project/pyproject.toml
new file mode 100644
index 00000000000..cddd15b1411
--- /dev/null
+++ b/tests/fixtures/file_scripts_project/pyproject.toml
@@ -0,0 +1,16 @@
+[tool.poetry]
+name = "file-scripts-project"
+version = "1.0.0"
+description = "A project with file scripts."
+authors = ["Test Author "]
+
+[tool.poetry.dependencies]
+python = "^3.7"
+
+[tool.poetry.scripts]
+my-script = { reference = "bin/my-script.sh", type = "file" }
+console-entry = "file_scripts_project:main"
+
+[build-system]
+requires = ["poetry-core"]
+build-backend = "poetry.core.masonry.api"
diff --git a/tests/masonry/builders/test_editable_builder.py b/tests/masonry/builders/test_editable_builder.py
index 958c33c4350..2d675721a5d 100644
--- a/tests/masonry/builders/test_editable_builder.py
+++ b/tests/masonry/builders/test_editable_builder.py
@@ -422,3 +422,123 @@ def test_builder_catches_bad_scripts_too_many_colon(
assert "foo::bar" in msg
# and some hint about what is wrong
assert "Too many" in msg
+
+
+@pytest.fixture()
+def file_scripts_poetry(fixture_dir: FixtureDirGetter) -> Poetry:
+ poetry = Factory().create_poetry(fixture_dir("file_scripts_project"))
+ return poetry
+
+
+def test_builder_installs_file_scripts(
+ file_scripts_poetry: Poetry,
+ tmp_path: Path,
+) -> None:
+ env_manager = EnvManager(file_scripts_poetry)
+ venv_path = tmp_path / "venv"
+ env_manager.build_venv(venv_path)
+ tmp_venv = VirtualEnv(venv_path)
+
+ builder = EditableBuilder(file_scripts_poetry, tmp_venv, NullIO())
+ builder.build()
+
+ # The file script should be copied to the venv bin directory
+ script_path = tmp_venv._bin_dir.joinpath("my-script")
+ assert script_path.exists(), (
+ f"File script 'my-script' was not copied to {tmp_venv._bin_dir}"
+ )
+
+ # Check script content matches the source
+ source_content = (
+ file_scripts_poetry.file.path.parent / "bin" / "my-script.sh"
+ ).read_text(encoding="utf-8")
+ assert script_path.read_text(encoding="utf-8") == source_content
+
+ # Check the file is executable
+ assert os.access(script_path, os.X_OK)
+
+ # The console entry point should also be installed
+ console_script = tmp_venv._bin_dir.joinpath("console-entry")
+ assert console_script.exists(), (
+ f"Console script 'console-entry' was not installed to {tmp_venv._bin_dir}"
+ )
+
+
+def test_builder_skips_missing_file_script(
+ fixture_dir: FixtureDirGetter,
+ tmp_path: Path,
+) -> None:
+ from cleo.io.buffered_io import BufferedIO
+
+ poetry = Factory().create_poetry(fixture_dir("file_scripts_missing_ref_project"))
+ env_manager = EnvManager(poetry)
+ venv_path = tmp_path / "venv"
+ env_manager.build_venv(venv_path)
+ tmp_venv = VirtualEnv(venv_path)
+
+ io = BufferedIO()
+ builder = EditableBuilder(poetry, tmp_venv, io)
+ builder.build()
+
+ # The file script for the missing reference must not be created
+ script_path = tmp_venv._bin_dir.joinpath("missing-script")
+ assert not script_path.exists()
+
+ # The error message should be logged
+ error_output = io.fetch_error()
+ assert "missing-script" in error_output
+ assert "does not exist" in error_output
+
+
+def test_builder_skips_directory_file_script(
+ fixture_dir: FixtureDirGetter,
+ tmp_path: Path,
+) -> None:
+ from cleo.io.buffered_io import BufferedIO
+
+ poetry = Factory().create_poetry(fixture_dir("file_scripts_dir_ref_project"))
+ env_manager = EnvManager(poetry)
+ venv_path = tmp_path / "venv"
+ env_manager.build_venv(venv_path)
+ tmp_venv = VirtualEnv(venv_path)
+
+ io = BufferedIO()
+ builder = EditableBuilder(poetry, tmp_venv, io)
+ builder.build()
+
+ # The file script for the directory reference must not be created
+ script_path = tmp_venv._bin_dir.joinpath("dir-script")
+ assert not script_path.exists()
+
+ # The error message should be logged
+ error_output = io.fetch_error()
+ assert "dir-script" in error_output
+ assert "is not a file" in error_output
+
+
+def test_builder_skips_file_script_missing_reference_field(
+ fixture_dir: FixtureDirGetter,
+ tmp_path: Path,
+) -> None:
+ from cleo.io.buffered_io import BufferedIO
+
+ poetry = Factory().create_poetry(
+ fixture_dir("file_scripts_no_ref_field_project")
+ )
+ env_manager = EnvManager(poetry)
+ venv_path = tmp_path / "venv"
+ env_manager.build_venv(venv_path)
+ tmp_venv = VirtualEnv(venv_path)
+
+ io = BufferedIO()
+ builder = EditableBuilder(poetry, tmp_venv, io)
+ builder.build()
+
+ # The file script with missing reference field must not be created
+ script_path = tmp_venv._bin_dir.joinpath("no-ref-script")
+ assert not script_path.exists()
+
+ # The error message should be logged
+ error_output = io.fetch_error()
+ assert "no-ref-script" in error_output
+ assert "reference" in error_output