Repository: pdm-project/pdm

Issue:
 pdm install --no-lock fails in the absence of lock file
- [x] I have searched the issue tracker and believe that this is not a duplicate.


## Steps to reproduce

`rm pdm.lock`
`pdm install --no-lock -v`

## Actual behavior

```
$ rm pdm.lock
$ pdm install --no-lock -v
Lock file does not exist
STATUS: Resolving packages from lockfile...
Traceback (most recent call last):
  File "/Users/mathieu/.local/bin/pdm", line 8, in <module>
    sys.exit(main())
             ^^^^^^
  File "/Users/mathieu/.local/pipx/venvs/pdm/lib/python3.11/site-packages/pdm/core.py", line 290, in main
    return Core().main(args or sys.argv[1:])
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/mathieu/.local/pipx/venvs/pdm/lib/python3.11/site-packages/pdm/core.py", line 208, in main
    raise cast(Exception, err).with_traceback(traceback) from None
  File "/Users/mathieu/.local/pipx/venvs/pdm/lib/python3.11/site-packages/pdm/core.py", line 203, in main
    self.handle(project, options)
  File "/Users/mathieu/.local/pipx/venvs/pdm/lib/python3.11/site-packages/pdm/core.py", line 157, in handle
    command.handle(project, options)
  File "/Users/mathieu/.local/pipx/venvs/pdm/lib/python3.11/site-packages/pdm/cli/commands/install.py", line 95, in handle
    actions.do_sync(
  File "/Users/mathieu/.local/pipx/venvs/pdm/lib/python3.11/site-packages/pdm/cli/actions.py", line 197, in do_sync
    candidates = resolve_candidates_from_lockfile(project, requirements)
                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/mathieu/.local/pipx/venvs/pdm/lib/python3.11/site-packages/pdm/cli/actions.py", line 140, in resolve_candidates_from_lockfile
    mapping, *_ = resolve(
                  ^^^^^^^^
  File "/Users/mathieu/.local/pipx/venvs/pdm/lib/python3.11/site-packages/pdm/resolver/core.py", line 36, in resolve
    result = resolver.resolve(requirements, max_rounds)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/mathieu/.local/pipx/venvs/pdm/lib/python3.11/site-packages/resolvelib/resolvers.py", line 546, in resolve
    state = resolution.resolve(requirements, max_rounds=max_rounds)
            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/mathieu/.local/pipx/venvs/pdm/lib/python3.11/site-packages/resolvelib/resolvers.py", line 399, in resolve
    raise ResolutionImpossible(e.criterion.information)
resolvelib.resolvers.ResolutionImpossible: [RequirementInformation(requirement=NamedRequirement(name='jinja2', marker=None, extras=set(), specifier=<SpecifierSet('')>, editable=False, prerelease=False), parent=None)]
```

## Expected behavior

install succeeds

## Environment Information

```
 pdm info ; pdm info --env
PDM version:
  2.9.1
Python Interpreter:
  /Users/mathieu/.pyenv/versions/3.10.12/bin/python3 (3.10)
Project Root:
  /Users/mathieu/dev/ggcore
Local Packages:
  /Users/mathieu/dev/ggcore/__pypackages__/3.10
{
  "implementation_name": "cpython",
  "implementation_version": "3.10.12",
  "os_name": "posix",
  "platform_machine": "arm64",
  "platform_release": "22.6.0",
  "platform_system": "Darwin",
  "platform_version": "Darwin Kernel Version 22.6.0: Wed Jul  5 22:17:35 PDT 2023; root:xnu-8796.141.3~6/RELEASE_ARM64_T8112",
  "python_full_version": "3.10.12",
  "platform_python_implementation": "CPython",
  "python_version": "3.10",
  "sys_platform": "darwin"
}

```


Discussion:
codecov[bot]: ## [Codecov](https://app.codecov.io/gh/pdm-project/pdm/pull/2250?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pdm-project) Report
Patch coverage is **`100.00%`** of modified lines.

| [Files Changed](https://app.codecov.io/gh/pdm-project/pdm/pull/2250?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pdm-project) | Coverage |
|---|---|
| [src/pdm/cli/commands/update.py](https://app.codecov.io/gh/pdm-project/pdm/pull/2250?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pdm-project#diff-c3JjL3BkbS9jbGkvY29tbWFuZHMvdXBkYXRlLnB5) | `ø` |
| [src/pdm/cli/actions.py](https://app.codecov.io/gh/pdm-project/pdm/pull/2250?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pdm-project#diff-c3JjL3BkbS9jbGkvYWN0aW9ucy5weQ==) | `100.00%` |
| [src/pdm/cli/commands/add.py](https://app.codecov.io/gh/pdm-project/pdm/pull/2250?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pdm-project#diff-c3JjL3BkbS9jbGkvY29tbWFuZHMvYWRkLnB5) | `100.00%` |
| [src/pdm/cli/commands/install.py](https://app.codecov.io/gh/pdm-project/pdm/pull/2250?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pdm-project#diff-c3JjL3BkbS9jbGkvY29tbWFuZHMvaW5zdGFsbC5weQ==) | `100.00%` |
| [src/pdm/cli/commands/remove.py](https://app.codecov.io/gh/pdm-project/pdm/pull/2250?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pdm-project#diff-c3JjL3BkbS9jbGkvY29tbWFuZHMvcmVtb3ZlLnB5) | `100.00%` |
| [src/pdm/cli/options.py](https://app.codecov.io/gh/pdm-project/pdm/pull/2250?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pdm-project#diff-c3JjL3BkbS9jbGkvb3B0aW9ucy5weQ==) | `100.00%` |
| [src/pdm/installers/synchronizers.py](https://app.codecov.io/gh/pdm-project/pdm/pull/2250?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pdm-project#diff-c3JjL3BkbS9pbnN0YWxsZXJzL3N5bmNocm9uaXplcnMucHk=) | `100.00%` |
| [src/pdm/project/core.py](https://app.codecov.io/gh/pdm-project/pdm/pull/2250?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pdm-project#diff-c3JjL3BkbS9wcm9qZWN0L2NvcmUucHk=) | `100.00%` |


:loudspeaker: Thoughts on this report? [Let us know!](https://about.codecov.io/pull-request-comment-report/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pdm-project).


Patch:
diff --git a/news/2245.feature.md b/news/2245.feature.md
new file mode 100644
index 00000000..186c791a
--- /dev/null
+++ b/news/2245.feature.md
@@ -0,0 +1,1 @@
+Fix an issue that `--no-lock` option doesn't work as expected. Also support `--no-lock` option for `add`, `remove` and `update` commands.
diff --git a/src/pdm/cli/actions.py b/src/pdm/cli/actions.py
index 20cc1b9c..565372e4 100644
--- a/src/pdm/cli/actions.py
+++ b/src/pdm/cli/actions.py
@@ -119,7 +119,8 @@ def do_lock(
             data = format_lockfile(
                 project, mapping, dependencies, groups=groups, cross_platform=cross_platform, static_urls=static_urls
             )
-            ui.echo(f"{termui.Emoji.LOCK} Lock successful")
+            if project.enable_write_lockfile:
+                ui.echo(f"{termui.Emoji.LOCK} Lock successful")
             project.write_lockfile(data, write=not dry_run)
             hooks.try_emit("post_lock", resolution=mapping, dry_run=dry_run)
 
diff --git a/src/pdm/cli/commands/add.py b/src/pdm/cli/commands/add.py
index 7f882c1f..1f0f0532 100644
--- a/src/pdm/cli/commands/add.py
+++ b/src/pdm/cli/commands/add.py
@@ -10,6 +10,7 @@ from pdm.cli.options import (
     dry_run_option,
     install_group,
     lockfile_option,
+    no_lock_option,
     packages_group,
     prerelease_option,
     save_strategy_group,
@@ -33,6 +34,7 @@ class Command(BaseCommand):
     arguments = (
         *BaseCommand.arguments,
         lockfile_option,
+        no_lock_option,
         save_strategy_group,
         update_strategy_group,
         prerelease_option,
@@ -169,10 +171,10 @@ class Command(BaseCommand):
         # Update dependency specifiers and lockfile hash.
         deps_to_update = group_deps if unconstrained else requirements
         save_version_specifiers({group: deps_to_update}, resolved, save)
+        hooks.try_emit("post_lock", resolution=resolved, dry_run=dry_run)
         if not dry_run:
             project.add_dependencies(deps_to_update, group, selection.dev or False)
             project.write_lockfile(project.lockfile._data, False)
-            hooks.try_emit("post_lock", resolution=resolved, dry_run=dry_run)
         populate_requirement_names(group_deps)
         if sync:
             do_sync(
diff --git a/src/pdm/cli/commands/install.py b/src/pdm/cli/commands/install.py
index 3b1ac841..3b41c80a 100644
--- a/src/pdm/cli/commands/install.py
+++ b/src/pdm/cli/commands/install.py
@@ -6,7 +6,15 @@ from pdm.cli import actions
 from pdm.cli.commands.base import BaseCommand
 from pdm.cli.filters import GroupSelection
 from pdm.cli.hooks import HookManager
-from pdm.cli.options import dry_run_option, groups_group, install_group, lockfile_option, skip_option, venv_option
+from pdm.cli.options import (
+    dry_run_option,
+    groups_group,
+    install_group,
+    lockfile_option,
+    no_lock_option,
+    skip_option,
+    venv_option,
+)
 from pdm.project import Project
 
 
@@ -19,18 +27,12 @@ class Command(BaseCommand):
         install_group,
         dry_run_option,
         lockfile_option,
+        no_lock_option,
         skip_option,
         venv_option,
     )
 
     def add_arguments(self, parser: argparse.ArgumentParser) -> None:
-        parser.add_argument(
-            "--no-lock",
-            dest="lock",
-            action="store_false",
-            default=True,
-            help="Don't do lock if the lock file is not found or outdated",
-        )
         parser.add_argument(
             "--check",
             action="store_true",
@@ -79,18 +81,21 @@ class Command(BaseCommand):
                     err=True,
                 )
                 sys.exit(1)
-            if options.lock:
+            if project.enable_write_lockfile:
                 project.core.ui.echo("Updating the lock file...", style="success", err=True)
-                unseleted = GroupSelection(project)
-                actions.do_lock(
-                    project,
-                    strategy=strategy,
-                    dry_run=options.dry_run,
-                    hooks=hooks,
-                    # We would like to keep the selected groups when the lockfile exists
-                    # but use the groups passed-in when creating a new lockfile.
-                    groups=unseleted.all() if strategy != "all" else selection.all(),
-                )
+            # We would like to keep the selected groups when the lockfile exists
+            # but use the groups passed-in when creating a new lockfile or doing a non-lock install.
+            if strategy == "all" or not project.enable_write_lockfile:
+                lock_selection = selection
+            else:
+                lock_selection = GroupSelection(project)
+            actions.do_lock(
+                project,
+                strategy=strategy,
+                dry_run=options.dry_run,
+                hooks=hooks,
+                groups=lock_selection.all(),
+            )
 
         actions.do_sync(
             project,
diff --git a/src/pdm/cli/commands/remove.py b/src/pdm/cli/commands/remove.py
index 380fc76b..55f7cecc 100644
--- a/src/pdm/cli/commands/remove.py
+++ b/src/pdm/cli/commands/remove.py
@@ -6,7 +6,7 @@ from typing import TYPE_CHECKING, cast
 from pdm.cli.commands.base import BaseCommand
 from pdm.cli.filters import GroupSelection
 from pdm.cli.hooks import HookManager
-from pdm.cli.options import dry_run_option, install_group, lockfile_option, skip_option, venv_option
+from pdm.cli.options import dry_run_option, install_group, lockfile_option, no_lock_option, skip_option, venv_option
 from pdm.exceptions import PdmUsageError, ProjectError
 
 if TYPE_CHECKING:
@@ -18,7 +18,15 @@ if TYPE_CHECKING:
 class Command(BaseCommand):
     """Remove packages from pyproject.toml"""
 
-    arguments = (*BaseCommand.arguments, install_group, dry_run_option, lockfile_option, skip_option, venv_option)
+    arguments = (
+        *BaseCommand.arguments,
+        install_group,
+        dry_run_option,
+        lockfile_option,
+        no_lock_option,
+        skip_option,
+        venv_option,
+    )
 
     def add_arguments(self, parser: argparse.ArgumentParser) -> None:
         parser.add_argument(
diff --git a/src/pdm/cli/commands/update.py b/src/pdm/cli/commands/update.py
index 6ac98877..267de1c7 100644
--- a/src/pdm/cli/commands/update.py
+++ b/src/pdm/cli/commands/update.py
@@ -11,6 +11,7 @@ from pdm.cli.options import (
     groups_group,
     install_group,
     lockfile_option,
+    no_lock_option,
     prerelease_option,
     save_strategy_group,
     skip_option,
@@ -35,6 +36,7 @@ class Command(BaseCommand):
         groups_group,
         install_group,
         lockfile_option,
+        no_lock_option,
         save_strategy_group,
         update_strategy_group,
         prerelease_option,
diff --git a/src/pdm/cli/completions/pdm.bash b/src/pdm/cli/completions/pdm.bash
index aa3c5028..289d888d 100644
--- a/src/pdm/cli/completions/pdm.bash
+++ b/src/pdm/cli/completions/pdm.bash
@@ -29,7 +29,7 @@ _pdm_a919b69078acdf0a_complete()
         case "$com" in
 
             (add)
-            opts="--dev --dry-run --editable --fail-fast --global --group --help --lockfile --no-editable --no-isolation --no-self --no-sync --prerelease --project --save-compatible --save-exact --save-minimum --save-wildcard --skip --unconstrained --update-all --update-eager --update-reuse --venv --verbose"
+            opts="--dev --dry-run --editable --fail-fast --global --group --help --lockfile --no-editable --no-isolation --no-lock --no-self --no-sync --prerelease --project --save-compatible --save-exact --save-minimum --save-wildcard --skip --unconstrained --update-all --update-eager --update-reuse --venv --verbose"
             ;;
 
             (build)
@@ -89,7 +89,7 @@ _pdm_a919b69078acdf0a_complete()
             ;;
 
             (remove)
-            opts="--dev --dry-run --fail-fast --global --group --help --lockfile --no-editable --no-isolation --no-self --no-sync --project --skip --venv --verbose"
+            opts="--dev --dry-run --fail-fast --global --group --help --lockfile --no-editable --no-isolation --no-lock --no-self --no-sync --project --skip --venv --verbose"
             ;;
 
             (run)
@@ -113,7 +113,7 @@ _pdm_a919b69078acdf0a_complete()
             ;;
 
             (update)
-            opts="--dev --fail-fast --global --group --help --lockfile --no-default --no-editable --no-isolation --no-self --no-sync --outdated --prerelease --production --project --save-compatible --save-exact --save-minimum --save-wildcard --skip --top --unconstrained --update-all --update-eager --update-reuse --venv --verbose"
+            opts="--dev --fail-fast --global --group --help --lockfile --no-default --no-editable --no-isolation --no-lock --no-self --no-sync --outdated --prerelease --production --project --save-compatible --save-exact --save-minimum --save-wildcard --skip --top --unconstrained --update-all --update-eager --update-reuse --venv --verbose"
             ;;
 
             (use)
diff --git a/src/pdm/cli/completions/pdm.fish b/src/pdm/cli/completions/pdm.fish
index cc3cf7ea..187e5271 100644
--- a/src/pdm/cli/completions/pdm.fish
+++ b/src/pdm/cli/completions/pdm.fish
@@ -31,6 +31,7 @@ complete -c pdm -A -n '__fish_seen_subcommand_from add' -l help -d 'Show this he
 complete -c pdm -A -n '__fish_seen_subcommand_from add' -l lockfile -d 'Specify another lockfile path. Default: pdm.lock. [env var: PDM_LOCKFILE]'
 complete -c pdm -A -n '__fish_seen_subcommand_from add' -l no-editable -d 'Install non-editable versions for all packages'
 complete -c pdm -A -n '__fish_seen_subcommand_from add' -l no-isolation -d 'Do not isolate the build in a clean environment'
+complete -c pdm -A -n '__fish_seen_subcommand_from add' -l no-lock -d 'Don\'t try to create or update the lockfile. [env var: PDM_NO_LOCK]'
 complete -c pdm -A -n '__fish_seen_subcommand_from add' -l no-self -d 'Don\'t install the project itself. [env var: PDM_NO_SELF]'
 complete -c pdm -A -n '__fish_seen_subcommand_from add' -l no-sync -d 'Only write pyproject.toml and do not sync the working set'
 complete -c pdm -A -n '__fish_seen_subcommand_from add' -l prerelease -d 'Allow prereleases to be pinned'
@@ -176,7 +177,7 @@ complete -c pdm -A -n '__fish_seen_subcommand_from install' -l lockfile -d 'Spec
 complete -c pdm -A -n '__fish_seen_subcommand_from install' -l no-default -d 'Don\'t include dependencies from the default group'
 complete -c pdm -A -n '__fish_seen_subcommand_from install' -l no-editable -d 'Install non-editable versions for all packages'
 complete -c pdm -A -n '__fish_seen_subcommand_from install' -l no-isolation -d 'Do not isolate the build in a clean environment'
-complete -c pdm -A -n '__fish_seen_subcommand_from install' -l no-lock -d 'Don\'t do lock if the lock file is not found or outdated'
+complete -c pdm -A -n '__fish_seen_subcommand_from install' -l no-lock -d 'Don\'t try to create or update the lockfile. [env var: PDM_NO_LOCK]'
 complete -c pdm -A -n '__fish_seen_subcommand_from install' -l no-self -d 'Don\'t install the project itself. [env var: PDM_NO_SELF]'
 complete -c pdm -A -n '__fish_seen_subcommand_from install' -l plugins -d 'Install the plugins specified in pyproject.toml'
 complete -c pdm -A -n '__fish_seen_subcommand_from install' -l production -d 'Unselect dev dependencies'
@@ -283,6 +284,7 @@ complete -c pdm -A -n '__fish_seen_subcommand_from remove' -l help -d 'Show this
 complete -c pdm -A -n '__fish_seen_subcommand_from remove' -l lockfile -d 'Specify another lockfile path. Default: pdm.lock. [env var: PDM_LOCKFILE]'
 complete -c pdm -A -n '__fish_seen_subcommand_from remove' -l no-editable -d 'Install non-editable versions for all packages'
 complete -c pdm -A -n '__fish_seen_subcommand_from remove' -l no-isolation -d 'Do not isolate the build in a clean environment'
+complete -c pdm -A -n '__fish_seen_subcommand_from remove' -l no-lock -d 'Don\'t try to create or update the lockfile. [env var: PDM_NO_LOCK]'
 complete -c pdm -A -n '__fish_seen_subcommand_from remove' -l no-self -d 'Don\'t install the project itself. [env var: PDM_NO_SELF]'
 complete -c pdm -A -n '__fish_seen_subcommand_from remove' -l no-sync -d 'Only write pyproject.toml and do not uninstall packages'
 complete -c pdm -A -n '__fish_seen_subcommand_from remove' -l project -d 'Specify another path as the project root, which changes the base of pyproject.toml and __pypackages__ [env var: PDM_PROJECT]'
@@ -387,6 +389,7 @@ complete -c pdm -A -n '__fish_seen_subcommand_from update' -l lockfile -d 'Speci
 complete -c pdm -A -n '__fish_seen_subcommand_from update' -l no-default -d 'Don\'t include dependencies from the default group'
 complete -c pdm -A -n '__fish_seen_subcommand_from update' -l no-editable -d 'Install non-editable versions for all packages'
 complete -c pdm -A -n '__fish_seen_subcommand_from update' -l no-isolation -d 'Do not isolate the build in a clean environment'
+complete -c pdm -A -n '__fish_seen_subcommand_from update' -l no-lock -d 'Don\'t try to create or update the lockfile. [env var: PDM_NO_LOCK]'
 complete -c pdm -A -n '__fish_seen_subcommand_from update' -l no-self -d 'Don\'t install the project itself. [env var: PDM_NO_SELF]'
 complete -c pdm -A -n '__fish_seen_subcommand_from update' -l no-sync -d 'Only update lock file but do not sync packages'
 complete -c pdm -A -n '__fish_seen_subcommand_from update' -l outdated -d 'Show the difference only without modifying the lockfile content'
diff --git a/src/pdm/cli/completions/pdm.ps1 b/src/pdm/cli/completions/pdm.ps1
index 13c0e76e..eb188b78 100644
--- a/src/pdm/cli/completions/pdm.ps1
+++ b/src/pdm/cli/completions/pdm.ps1
@@ -211,7 +211,7 @@ function TabExpansion($line, $lastWord) {
                             "-d", "--dev", "--save-compatible", "--save-wildcard", "--dry-run", "--save-exact",
                             "--save-minimum", "--update-eager", "--update-reuse", "--update-all", "-g", "--global",
                             "--no-sync", "--no-editable", "--no-self", "-u", "--unconstrained", "--no-isolation",
-                            "--pre", "--prerelease", "-L", "--lockfile", "--fail-fast", "-x"
+                            "--pre", "--prerelease", "-L", "--lockfile", "--fail-fast", "-x", "--no-lock"
                         )),
                         $sectionOption,
                         $projectOption,
@@ -282,7 +282,10 @@ function TabExpansion($line, $lastWord) {
             "init" {
                 $completer.AddOpts(
                     @(
-                        [Option]::new(@("-g", "--global", "--non-interactive", "-n", "--python", "--lib", "--copier", "--cookiecutter", "--overwrite")),
+                        [Option]::new(@(
+                            "-g", "--global", "--non-interactive", "-n", "--python", "--lib", "--copier",
+                            "--cookiecutter", "--overwrite"
+                        )),
                         $projectOption,
                         $skipOption,
                         [Option]::new(@("--backend")).WithValues(@("pdm-backend", "setuptools", "flit", "hatching"))
@@ -306,7 +309,10 @@ function TabExpansion($line, $lastWord) {
             "list" {
                 $completer.AddOpts(
                     @(
-                        [Option]::new(@("--graph", "--tree", "--global", "-g", "--reverse", "-r", "--freeze", "--json", "--csv", "--markdown", "--fields", "--sort", "--include", "--exclude", "--resolve")),
+                        [Option]::new(@(
+                            "--graph", "--tree", "--global", "-g", "--reverse", "-r", "--freeze","--json", "--csv",
+                            "--markdown", "--fields", "--sort", "--include", "--exclude", "--resolve"
+                        )),
                         $venvOption,
                         $projectOption
                     ))
@@ -315,7 +321,10 @@ function TabExpansion($line, $lastWord) {
             "lock" {
                 $completer.AddOpts(
                     @(
-                        [Option]::new(@("--global", "-g", "--no-isolation", "--refresh", "-L", "--lockfile", "--check", "--dev", "--prod", "--production", "-d", "--no-default", "--no-cross-platform", "--static-urls", "--no-static-urls")),
+                        [Option]::new(@(
+                            "--global", "-g", "--no-isolation", "--refresh", "-L", "--lockfile", "--check", "--dev", "--prod",
+                            "--production", "-d", "--no-default", "--no-cross-platform", "--static-urls", "--no-static-urls"
+                        )),
                         $skipOption,
                         $sectionOption,
                         $projectOption
@@ -362,7 +371,7 @@ function TabExpansion($line, $lastWord) {
                     @(
                         [Option]::new(@(
                             "--global", "-g", "--dev", "-d", "--dry-run", "--no-sync", "--no-editable", "--no-self",
-                            "--no-isolation", "-L", "--lockfile", "--fail-fast", "-x"
+                            "--no-isolation", "-L", "--lockfile", "--fail-fast", "-x", "--no-lock"
                         )),
                         $projectOption,
                         $skipOption,
@@ -413,7 +422,7 @@ function TabExpansion($line, $lastWord) {
                             "-d", "--dev", "--save-compatible", "--prod", "--production", "--save-wildcard", "--save-exact",
                             "--save-minimum", "--update-eager", "--update-reuse", "--update-all", "-g", "--global", "--dry-run",
                             "--outdated", "--top", "-u", "--unconstrained", "--no-editable", "--no-self", "--no-isolation",
-                            "--no-sync", "--pre", "--prerelease", "-L", "--lockfile", "--fail-fast", "-x"
+                            "--no-sync", "--pre", "--prerelease", "-L", "--lockfile", "--fail-fast", "-x", "--no-lock"
                         )),
                         $sectionOption,
                         $skipOption,
diff --git a/src/pdm/cli/completions/pdm.zsh b/src/pdm/cli/completions/pdm.zsh
index 3ffdc6d9..42524051 100644
--- a/src/pdm/cli/completions/pdm.zsh
+++ b/src/pdm/cli/completions/pdm.zsh
@@ -73,6 +73,7 @@ _pdm() {
         '--update-all[Update all dependencies and sub-dependencies]'
         '--no-editable[Install non-editable versions for all packages]'
         "--no-self[Don't install the project itself]"
+        "--no-lock[Don't try to create or update the lockfile. [env var: PDM_NO_LOCK]]"
         '--venv[Run the command in the virtual environment with the given key. (env var: PDM_IN_VENV)]:venv:'
         {-k,--skip}'[Skip some tasks and/or hooks by their comma-separated names]'
         {-u,--unconstrained}'[Ignore the version constraint of packages]'
@@ -202,7 +203,7 @@ _pdm() {
         {-L,--lockfile}'[Specify another lockfile path, or use `PDM_LOCKFILE` env variable. Default: pdm.lock]:lockfile:_files'
         {--prod,--production}"[Unselect dev dependencies]"
         {-k,--skip}'[Skip some tasks and/or hooks by their comma-separated names]'
-        "--no-lock[Don't do lock if lock file is not found or outdated]"
+        "--no-lock[Don't try to create or update the lockfile. [env var: PDM_NO_LOCK]]"
         "--no-default[Don\'t include dependencies from the default group]"
         '--no-editable[Install non-editable versions for all packages]'
         "--no-self[Don't install the project itself]"
@@ -322,6 +323,7 @@ _pdm() {
         "--no-sync[Only write pyproject.toml and do not uninstall packages]"
         '--no-editable[Install non-editable versions for all packages]'
         "--no-self[Don't install the project itself]"
+        "--no-lock[Don't try to create or update the lockfile. [env var: PDM_NO_LOCK]]"
         {-x,--fail-fast}'[Abort on first installation error]'
         "--no-isolation[do not isolate the build in a clean environment]"
         "--dry-run[Show the difference only without modifying the lockfile content]"
@@ -399,6 +401,7 @@ _pdm() {
         '--no-editable[Install non-editable versions for all packages]'
         "--no-self[Don't install the project itself]"
         "--no-sync[Only update lock file but do not sync packages]"
+        "--no-lock[Don't try to create or update the lockfile. [env var: PDM_NO_LOCK]]"
         {-k,--skip}'[Skip some tasks and/or hooks by their comma-separated names]'
         {-u,--unconstrained}'[Ignore the version constraint of packages]'
         {--pre,--prerelease}'[Allow prereleases to be pinned]'
diff --git a/src/pdm/cli/options.py b/src/pdm/cli/options.py
index b558f918..ec5f5f4c 100644
--- a/src/pdm/cli/options.py
+++ b/src/pdm/cli/options.py
@@ -142,6 +142,11 @@ lockfile_option = Option(
 )
 
 
+@Option("--no-lock", nargs=0, help="Don't try to create or update the lockfile. [env var: PDM_NO_LOCK]")
+def no_lock_option(project: Project, namespace: argparse.Namespace, values: str | Sequence[Any] | None) -> None:
+    project.enable_write_lockfile = False
+
+
 @Option("--pep582", const="AUTO", metavar="SHELL", nargs="?", help="Print the command line to be eval'd by the shell")
 def pep582_option(project: Project, namespace: argparse.Namespace, values: str | Sequence[Any] | None) -> None:
     from pdm.cli.actions import print_pep582_command
diff --git a/src/pdm/installers/synchronizers.py b/src/pdm/installers/synchronizers.py
index 6966b28e..42dc74cb 100644
--- a/src/pdm/installers/synchronizers.py
+++ b/src/pdm/installers/synchronizers.py
@@ -349,7 +349,7 @@ class Synchronizer(BaseSynchronizer):
         if not any((add, update, remove)):
             self.ui.echo("All packages are synced to date, nothing to do.")
             return
-        results = ["[bold]Synchronizing working set with lock file[/]:"]
+        results = ["[bold]Synchronizing working set with resolved packages[/]:"]
         results.extend(
             [
                 f"[success]{len(add)}[/] to add,",
diff --git a/src/pdm/project/core.py b/src/pdm/project/core.py
index c39893a3..e49ce93e 100644
--- a/src/pdm/project/core.py
+++ b/src/pdm/project/core.py
@@ -100,6 +100,7 @@ class Project:
 
         self.root: Path = Path(root_path or "").absolute()
         self.is_global = is_global
+        self.enable_write_lockfile = os.getenv("PDM_NO_LOCK", "0").lower() not in ("1", "true")
         self.init_global_project()
 
     def __repr__(self) -> str:
@@ -487,7 +488,7 @@ class Project:
         toml_data["metadata"].update(self.get_lock_metadata())
         self.lockfile.set_data(toml_data)
 
-        if write:
+        if write and self.enable_write_lockfile:
             self.lockfile.write(show_message)
 
     def make_self_candidate(self, editable: bool = True) -> Candidate:


Test patch:
diff --git a/tests/cli/test_add.py b/tests/cli/test_add.py
index 98336081..dee227a5 100644
--- a/tests/cli/test_add.py
+++ b/tests/cli/test_add.py
@@ -24,6 +24,20 @@ def test_add_package(project, working_set, dev_option, pdm):
         assert package in working_set
 
 
+def test_add_package_no_lock(project, working_set, dev_option, pdm):
+    pdm(["add", *dev_option, "--no-lock", "-v", "requests"], obj=project, strict=True)
+    group = (
+        project.pyproject.settings["dev-dependencies"]["dev"]
+        if dev_option
+        else project.pyproject.metadata["dependencies"]
+    )
+
+    assert group[0] == "requests>=2.19.1"
+    assert not project.lockfile.exists()
+    for package in ("requests", "idna", "chardet", "urllib3", "certifi"):
+        assert package in working_set
+
+
 def test_add_command(project, pdm, mocker):
     do_add = mocker.patch("pdm.cli.commands.add.Command.do_add")
     pdm(["add", "requests"], obj=project)
diff --git a/tests/cli/test_install.py b/tests/cli/test_install.py
index c80b19f9..fd2f7d59 100644
--- a/tests/cli/test_install.py
+++ b/tests/cli/test_install.py
@@ -170,6 +170,15 @@ def test_install_with_dry_run(project, pdm, repository):
     assert "pytz 2019.3" in result.output
 
 
+def test_install_no_lock(project, pdm, working_set):
+    project.add_dependencies({"requests": parse_requirement("requests")}, "default")
+    result = pdm(["install", "--no-lock"], obj=project)
+    assert result.exit_code == 0
+    assert not project.lockfile.exists()
+    assert "urllib3" in working_set
+    assert "requests" in working_set
+
+
 def test_install_check(pdm, project, repository):
     result = pdm(["install", "--check"], obj=project)
     assert result.exit_code == 1
diff --git a/tests/cli/test_remove.py b/tests/cli/test_remove.py
index 61d5ef96..d34190e0 100644
--- a/tests/cli/test_remove.py
+++ b/tests/cli/test_remove.py
@@ -32,6 +32,15 @@ def test_remove_package(project, working_set, dev_option, pdm):
     assert "pytz" not in working_set
 
 
+def test_remove_package_no_lock(project, working_set, dev_option, pdm):
+    pdm(["add", *dev_option, "requests", "pytz"], obj=project, strict=True)
+    pdm(["remove", *dev_option, "--no-lock", "pytz"], obj=project, strict=True)
+    assert "pytz" not in working_set
+    project.lockfile.reload()
+    locked_candidates = project.locked_repository.all_candidates
+    assert "pytz" in locked_candidates
+
+
 def test_remove_package_with_dry_run(project, working_set, pdm):
     pdm(["add", "requests"], obj=project, strict=True)
     result = pdm(["remove", "requests", "--dry-run"], obj=project, strict=True)
diff --git a/tests/cli/test_update.py b/tests/cli/test_update.py
index 0c1cb506..52b52bf3 100644
--- a/tests/cli/test_update.py
+++ b/tests/cli/test_update.py
@@ -61,6 +61,15 @@ def test_update_all_packages(project, repository, pdm, strategy):
     assert "All packages are synced to date" in result.stdout
 
 
+def test_update_no_lock(project, working_set, repository, pdm):
+    pdm(["add", "pytz"], obj=project, strict=True)
+    repository.add_candidate("pytz", "2019.6")
+    pdm(["update", "--no-lock"], obj=project, strict=True)
+    assert working_set["pytz"].version == "2019.6"
+    project.lockfile.reload()
+    assert project.locked_repository.all_candidates["pytz"].version == "2019.3"
+
+
 @pytest.mark.usefixtures("working_set")
 def test_update_dry_run(project, repository, pdm):
     pdm(["add", "requests", "pytz"], obj=project, strict=True)