diff --git a/snapcraft/services/remotebuild.py b/snapcraft/services/remotebuild.py
index 3b53a25db2..c07c0dc38d 100644
--- a/snapcraft/services/remotebuild.py
+++ b/snapcraft/services/remotebuild.py
@@ -15,12 +15,47 @@
# along with this program. If not, see .
"""Snapcraft Lifecycle Service."""
+import pathlib
+import craft_cli
+import platformdirs
from craft_application import launchpad
from craft_application.services import remotebuild
+from typing_extensions import override
class RemoteBuild(remotebuild.RemoteBuildService):
"""Snapcraft remote build service."""
RecipeClass = launchpad.models.SnapRecipe
+
+ __credentials_filepath: pathlib.Path | None = None
+
+ @property
+ @override
+ def credentials_filepath(self) -> pathlib.Path:
+ """The filepath to the Launchpad credentials.
+
+ The legacy credentials are only loaded when they exist and the new credentials
+ do not exist. If the legacy credentials are loaded, emit a deprecation warning.
+ """
+ # return early so the deprecation notice is emitted only once
+ if self.__credentials_filepath:
+ return self.__credentials_filepath
+
+ credentials_filepath = super().credentials_filepath
+ legacy_credentials_filepath = (
+ platformdirs.user_data_path("snapcraft") / "provider/launchpad/credentials"
+ )
+
+ if not credentials_filepath.exists() and legacy_credentials_filepath.exists():
+ craft_cli.emit.progress(
+ f"Warning: Using launchpad credentials from deprecated location {str(legacy_credentials_filepath)!r}.\n"
+ f"Credentials should be migrated to {str(credentials_filepath)!r}.",
+ permanent=True,
+ )
+ self.__credentials_filepath = legacy_credentials_filepath
+ else:
+ self.__credentials_filepath = credentials_filepath
+
+ return self.__credentials_filepath
diff --git a/snapcraft_legacy/internal/remote_build/_launchpad.py b/snapcraft_legacy/internal/remote_build/_launchpad.py
index c042fb7c48..853c35ed5e 100644
--- a/snapcraft_legacy/internal/remote_build/_launchpad.py
+++ b/snapcraft_legacy/internal/remote_build/_launchpad.py
@@ -1,6 +1,6 @@
# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*-
#
-# Copyright (C) 2019 Canonical Ltd
+# Copyright (C) 2019,2024 Canonical Ltd
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 3 as
@@ -16,6 +16,8 @@
import gzip
import logging
+import pathlib
+import platformdirs
import os
import shutil
import time
@@ -111,7 +113,6 @@ def __init__(
self._cache_dir = self._create_cache_directory()
self._data_dir = self._create_data_directory()
- self._credentials = os.path.join(self._data_dir, "credentials")
self._lp: Launchpad = self.login()
self.user = self._lp.me.name
@@ -122,6 +123,25 @@ def __init__(
def architectures(self) -> Sequence[str]:
return self._architectures
+ @property
+ def _credentials_filepath(self) -> pathlib.Path:
+ """The filepath to the Launchpad credentials.
+
+ If the credentials file does not exist in the default location but exists in the
+ legacy location, emit a deprecation warning and return the legacy location.
+ """
+ credentials_filepath = platformdirs.user_data_path("snapcraft") / "launchpad-credentials"
+ legacy_credentials_filepath = platformdirs.user_data_path("snapcraft") / "provider/launchpad/credentials"
+
+ if not credentials_filepath.exists() and legacy_credentials_filepath.exists():
+ logger.warning(
+ f"Warning: Using launchpad credentials from deprecated location {str(legacy_credentials_filepath)!r}.\n"
+ f"Credentials should be migrated to {str(credentials_filepath)!r}."
+ )
+ return legacy_credentials_filepath
+
+ return credentials_filepath
+
@architectures.setter
def architectures(self, architectures: Sequence[str]) -> None:
self._lp_processors: Optional[Sequence[str]] = None
@@ -256,7 +276,7 @@ def login(self) -> Launchpad:
"snapcraft remote-build {}".format(snapcraft_legacy.__version__),
"production",
self._cache_dir,
- credentials_file=self._credentials,
+ credentials_file=self._credentials_filepath,
version="devel",
)
except (ConnectionRefusedError, TimeoutError):
diff --git a/tests/legacy/unit/remote_build/test_launchpad.py b/tests/legacy/unit/remote_build/test_launchpad.py
index 7c24733af1..ca141c47e6 100644
--- a/tests/legacy/unit/remote_build/test_launchpad.py
+++ b/tests/legacy/unit/remote_build/test_launchpad.py
@@ -1,6 +1,6 @@
# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*-
#
-# Copyright (C) 2019 Canonical Ltd
+# Copyright (C) 2019,2024 Canonical Ltd
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 3 as
@@ -16,6 +16,8 @@
import textwrap
from datetime import datetime, timedelta, timezone
+import pathlib
+import pytest
from unittest import mock
import fixtures
@@ -535,3 +537,27 @@ def test_push_source_tree_error(self):
self.assertRaises(
errors.LaunchpadGitPushError, self.lpc.push_source_tree, repo_dir
)
+
+
+@pytest.mark.parametrize(
+ ("new_exists", "legacy_exists", "expected"),
+ [
+ (False, False, pathlib.Path("launchpad-credentials")),
+ (False, True, pathlib.Path("provider/launchpad/credentials")),
+ (True, False, pathlib.Path("launchpad-credentials")),
+ (True, True, pathlib.Path("launchpad-credentials")),
+ ],
+)
+def test_credentials_filepaths(new_exists, legacy_exists, expected, mocker, new_dir):
+ """Load legacy credentials only when they exist and the new ones do not."""
+ mocker.patch.object(LaunchpadClient, "__init__", lambda x: None)
+ mocker.patch("platformdirs.user_data_path", return_value=new_dir)
+ if new_exists:
+ (new_dir / "launchpad-credentials").touch()
+ if legacy_exists:
+ (new_dir / "provider/launchpad/credentials").mkdir(parents=True)
+ (new_dir / "provider/launchpad/credentials").touch()
+
+ credentials_filepath = LaunchpadClient()._credentials_filepath
+
+ assert credentials_filepath == new_dir / expected
diff --git a/tests/unit/services/test_remotebuild.py b/tests/unit/services/test_remotebuild.py
new file mode 100644
index 0000000000..7e8db89fa3
--- /dev/null
+++ b/tests/unit/services/test_remotebuild.py
@@ -0,0 +1,46 @@
+# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*-
+#
+# Copyright 2024 Canonical Ltd.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 3 as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+
+"""Snapcraft RemoteBuild service tests."""
+
+import pathlib
+
+import pytest
+
+
+@pytest.mark.parametrize(
+ ("new_exists", "legacy_exists", "expected"),
+ [
+ (False, False, pathlib.Path("launchpad-credentials")),
+ (False, True, pathlib.Path("provider/launchpad/credentials")),
+ (True, False, pathlib.Path("launchpad-credentials")),
+ (True, True, pathlib.Path("launchpad-credentials")),
+ ],
+)
+def test_credentials_filepaths(
+ new_exists, legacy_exists, expected, remote_build_service, mocker, new_dir
+):
+ """Load legacy credentials only when they exist and the new ones do not."""
+ mocker.patch("platformdirs.user_data_path", return_value=new_dir)
+ if new_exists:
+ (new_dir / "launchpad-credentials").touch()
+ if legacy_exists:
+ (new_dir / "provider/launchpad/credentials").mkdir(parents=True)
+ (new_dir / "provider/launchpad/credentials").touch()
+
+ credentials_filepath = remote_build_service.credentials_filepath
+
+ assert credentials_filepath == new_dir / expected