Skip to content

Commit 3ecfeaf

Browse files
committed
feat: support reuse local existing versions while version solving
1 parent c4676ad commit 3ecfeaf

File tree

8 files changed

+324
-56
lines changed

8 files changed

+324
-56
lines changed

idf_component_manager/dependencies.py

Lines changed: 33 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
1-
# SPDX-FileCopyrightText: 2022-2023 Espressif Systems (Shanghai) CO LTD
1+
# SPDX-FileCopyrightText: 2022-2024 Espressif Systems (Shanghai) CO LTD
22
# SPDX-License-Identifier: Apache-2.0
3-
43
import os
54
import shutil
65
from functools import total_ordering
@@ -12,7 +11,7 @@
1211
from idf_component_manager.version_solver.mixology.failure import SolverFailure
1312
from idf_component_manager.version_solver.mixology.package import Package
1413
from idf_component_manager.version_solver.version_solver import VersionSolver
15-
from idf_component_tools.build_system_tools import build_name
14+
from idf_component_tools.build_system_tools import build_name, get_idf_version
1615
from idf_component_tools.environment import getenv_bool
1716
from idf_component_tools.errors import (
1817
ComponentModifiedError,
@@ -25,11 +24,13 @@
2524
validate_managed_component_hash,
2625
)
2726
from idf_component_tools.lock import LockManager
28-
from idf_component_tools.manifest import ProjectRequirements
27+
from idf_component_tools.manifest import ComponentVersion, ProjectRequirements
2928
from idf_component_tools.manifest.solved_component import SolvedComponent
3029
from idf_component_tools.manifest.solved_manifest import SolvedManifest
3130
from idf_component_tools.messages import hint, warn
3231
from idf_component_tools.registry.api_client_errors import NetworkConnectionError
32+
from idf_component_tools.semver import Version
33+
from idf_component_tools.sources import IDFSource
3334
from idf_component_tools.sources.fetcher import ComponentFetcher
3435

3536

@@ -104,6 +105,14 @@ def is_solve_required(project_requirements, solution):
104105
print_info('Manifest files have changed, solving dependencies.')
105106
return True
106107

108+
if solution.idf_version and solution.idf_version != Version(get_idf_version()):
109+
print_info(
110+
'IDF version changed from {} to {}, solving dependencies.'.format(
111+
solution.idf_version, get_idf_version()
112+
)
113+
)
114+
return True
115+
107116
if solution.target and project_requirements.target != solution.target:
108117
print_info(
109118
'Target changed from {} to {}, solving dependencies.'.format(
@@ -130,14 +139,14 @@ def is_solve_required(project_requirements, solution):
130139
)
131140
except FetchingError:
132141
print_warn(
133-
'Version {} of dependency {} not found, probably it was deleted, solving dependencies.'.format(
134-
component.version, component.name
135-
)
142+
'Version {} of dependency {} not found, probably it was deleted, '
143+
'solving dependencies.'.format(component.version, component.name)
136144
)
137145
return True
138146
except NetworkConnectionError:
139147
hint(
140-
'Cannot establish a connection to the component registry. Skipping checks of dependency changes.'
148+
'Cannot establish a connection to the component registry. '
149+
'Skipping checks of dependency changes.'
141150
)
142151
return False
143152

@@ -222,7 +231,7 @@ def abs_posix_path(self): # type: () -> str
222231
def check_for_new_component_versions(project_requirements, old_solution):
223232
if getenv_bool('IDF_COMPONENT_CHECK_NEW_VERSION', False):
224233
# Check for newer versions of components
225-
solver = VersionSolver(project_requirements, old_solution)
234+
solver = VersionSolver(project_requirements)
226235
try:
227236
new_solution = solver.solve()
228237
new_deps_names = [dep.name for dep in new_solution.dependencies]
@@ -246,9 +255,8 @@ def check_for_new_component_versions(project_requirements, old_solution):
246255
if updateable_components_messages:
247256
hint(
248257
'\nFollowing dependencies have new versions available:\n{}'
249-
'\nConsider running "idf.py update-dependencies" to update your lock file.\n'.format(
250-
'\n'.join(updateable_components_messages)
251-
)
258+
'\nConsider running "idf.py update-dependencies" '
259+
'to update your lock file.\n'.format('\n'.join(updateable_components_messages))
252260
)
253261

254262
except (SolverFailure, NetworkConnectionError):
@@ -262,10 +270,18 @@ def download_project_dependencies(
262270
"""Solves dependencies and download components"""
263271
lock_manager = LockManager(lock_path)
264272
solution = lock_manager.load()
273+
265274
check_manifests_targets(project_requirements)
266275

267276
if is_solve_required(project_requirements, solution):
268-
solver = VersionSolver(project_requirements, solution, component_solved_callback=print_dot)
277+
# replace the old solution with the current idf
278+
for dep in solution.dependencies:
279+
if dep.name == IDFSource().NAME:
280+
dep.version = ComponentVersion(get_idf_version())
281+
282+
solver = VersionSolver(
283+
project_requirements, old_solution=solution, component_solved_callback=print_dot
284+
)
269285

270286
try:
271287
solution = solver.solve()
@@ -274,11 +290,11 @@ def download_project_dependencies(
274290
components_introduce_conflict = []
275291
for conflict_constraint in conflict_constraints:
276292
for manifest in project_requirements.manifests:
277-
for dep in manifest.dependencies:
278-
for source in dep.sources:
293+
for req in manifest.dependencies:
294+
for source in req.sources:
279295
if Package(
280-
dep.name, source
281-
) == conflict_constraint.package and dep.version_spec == str(
296+
req.name, source
297+
) == conflict_constraint.package and req.version_spec == str(
282298
conflict_constraint.constraint
283299
):
284300
components_introduce_conflict.append(manifest.name)

idf_component_manager/version_solver/version_solver.py

Lines changed: 67 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
# SPDX-FileCopyrightText: 2022-2024 Espressif Systems (Shanghai) CO LTD
22
# SPDX-License-Identifier: Apache-2.0
3-
3+
import logging
44
import os
55

66
from idf_component_tools.errors import DependencySolveError, FetchingError, SolverError
7+
from idf_component_tools.lock.manager import EMPTY_LOCK
78
from idf_component_tools.manifest import (
89
ComponentRequirement,
910
ComponentWithVersions,
@@ -17,6 +18,7 @@
1718

1819
from ..utils import print_info, print_warn
1920
from .helper import PackageSource
21+
from .mixology.failure import SolverFailure
2022
from .mixology.package import Package
2123
from .mixology.version_solver import VersionSolver as Solver
2224

@@ -25,6 +27,8 @@
2527
except ImportError:
2628
pass
2729

30+
logger = logging.getLogger(__name__)
31+
2832

2933
class VersionSolver(object):
3034
"""
@@ -38,18 +42,21 @@ def __init__(self, requirements, old_solution=None, component_solved_callback=No
3842
self.old_solution = old_solution
3943
self.component_solved_callback = component_solved_callback
4044

45+
self._init()
46+
47+
def _init(self):
48+
# put all the intermediate generated attrs here
49+
# to reset them when the solver is re-used
4150
self._source = PackageSource()
4251
self._solver = Solver(self._source)
4352
self._target = None
4453
self._overriders = set() # type: set[str]
4554
self._local_root_requirements = dict() # type: dict[str, ComponentRequirement]
46-
self._solved_requirements = set() # type: set[ComponentRequirement]
55+
self._parse_local_root_requirements()
4756

48-
def solve(self): # type: () -> SolvedManifest
49-
# scan all root local requirements
50-
# root local requirements defined in the file system manifest files
51-
# would have higher priorities
57+
self._solved_requirements = set() # type: set[ComponentRequirement]
5258

59+
def _parse_local_root_requirements(self): # type: () -> None
5360
# scan all root local requirements
5461
for manifest in self.requirements.manifests:
5562
for requirement in manifest.dependencies: # type: ComponentRequirement
@@ -68,7 +75,7 @@ def solve(self): # type: () -> SolvedManifest
6875
else:
6976
self._local_root_requirements[requirement.build_name] = requirement
7077

71-
# scan all root local components
78+
# add all local components, except [0] -> main component
7279
for manifest in self.requirements.manifests[1:]:
7380
# add itself as highest priority component
7481
if manifest.name and manifest.version and manifest._manifest_manager:
@@ -87,8 +94,17 @@ def solve(self): # type: () -> SolvedManifest
8794
version_spec=str(manifest.version),
8895
)
8996

97+
def _solve(self, cur_solution=None): # type: (SolvedManifest | None) -> SolvedManifest
98+
"""
99+
Solve the version requirements and return the result.
100+
101+
:param cur_solution: The current solution to be used as a starting point.
102+
:raises SolverError: If the solver fails to solve the requirements.
103+
"""
104+
# root local requirements defined in the file system manifest files
105+
# would have higher priorities
90106
for manifest in self.requirements.manifests:
91-
self.solve_manifest(manifest)
107+
self.solve_manifest(manifest, cur_solution=cur_solution)
92108

93109
self._source.override_dependencies(self._overriders)
94110

@@ -108,18 +124,42 @@ def solve(self): # type: () -> SolvedManifest
108124
solved_components, self.requirements.manifest_hash, self.requirements.target
109125
)
110126

127+
def solve(self): # type: () -> SolvedManifest
128+
if self.old_solution != SolvedManifest.fromdict(EMPTY_LOCK):
129+
try:
130+
return self._solve(self.old_solution)
131+
except SolverFailure as e:
132+
logger.debug(
133+
'Solver failed to solve the requirements with the current solution. '
134+
'Error: %s.\n'
135+
'Retrying without the current solution. ',
136+
e,
137+
)
138+
139+
self._init()
140+
return self._solve()
141+
111142
def get_versions_from_sources(
112-
self, requirement
113-
): # type: (ComponentRequirement) -> tuple[ComponentWithVersions | None, BaseSource | None]
143+
self,
144+
requirement, # type: ComponentRequirement
145+
cur_solution=None, # type: SolvedManifest | None
146+
): # type: (...) -> tuple[ComponentWithVersions | None, BaseSource | None]
114147
latest_source = None
115148
cmp_with_versions = None
116149
for source in requirement.sources:
117150
try:
118-
cmp_with_versions = source.versions(
119-
name=requirement.name,
120-
spec=requirement.version_spec,
121-
target=self.requirements.target,
122-
)
151+
if cur_solution and requirement.name in cur_solution.solved_components:
152+
cmp_with_versions = requirement.source.versions(
153+
name=requirement.name,
154+
spec=str(cur_solution.solved_components[requirement.name].version),
155+
target=self.requirements.target,
156+
)
157+
else:
158+
cmp_with_versions = source.versions(
159+
name=requirement.name,
160+
spec=requirement.version_spec,
161+
target=self.requirements.target,
162+
)
123163
latest_source = source
124164
if cmp_with_versions.versions:
125165
break
@@ -129,18 +169,22 @@ def get_versions_from_sources(
129169
pass
130170
return cmp_with_versions, latest_source
131171

132-
def solve_manifest(self, manifest): # type: (Manifest) -> None
172+
def solve_manifest(
173+
self,
174+
manifest, # type: Manifest
175+
cur_solution=None, # type: SolvedManifest | None
176+
): # type: (...) -> None
133177
for dep in self._dependencies_with_local_precedence(
134178
manifest.dependencies, manifest_path=manifest.path
135179
):
136180
if len(dep.sources) == 1:
137181
source = dep.source
138182
else:
139-
_, source = self.get_versions_from_sources(dep)
183+
_, source = self.get_versions_from_sources(dep, cur_solution=cur_solution)
140184

141185
self._source.root_dep(Package(dep.name, source), dep.version_spec)
142186
try:
143-
self.solve_component(dep, manifest_path=manifest.path)
187+
self.solve_component(dep, manifest_path=manifest.path, cur_solution=cur_solution)
144188
except DependencySolveError as e:
145189
raise SolverError(
146190
'Solver failed processing dependency "{dependency}" '
@@ -155,12 +199,14 @@ def solve_manifest(self, manifest): # type: (Manifest) -> None
155199
)
156200

157201
def solve_component(
158-
self, requirement, manifest_path=None
159-
): # type: (ComponentRequirement, str | None) -> None
202+
self, requirement, manifest_path=None, cur_solution=None
203+
): # type: (ComponentRequirement, str | None, SolvedManifest | None) -> None
160204
if requirement in self._solved_requirements:
161205
return
162206

163-
cmp_with_versions, source = self.get_versions_from_sources(requirement)
207+
cmp_with_versions, source = self.get_versions_from_sources(
208+
requirement, cur_solution=cur_solution
209+
)
164210

165211
if not cmp_with_versions or not cmp_with_versions.versions or not source:
166212
print_warn('Component "{}" not found'.format(requirement.name))

idf_component_tools/lock/manager.py

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# SPDX-FileCopyrightText: 2022-2023 Espressif Systems (Shanghai) CO LTD
1+
# SPDX-FileCopyrightText: 2022-2024 Espressif Systems (Shanghai) CO LTD
22
# SPDX-License-Identifier: Apache-2.0
33

44
import os
@@ -18,6 +18,7 @@
1818
from ..manifest import ComponentVersion, known_targets
1919
from ..manifest.solved_component import SolvedComponent
2020
from ..manifest.solved_manifest import SolvedManifest
21+
from ..messages import notice
2122
from ..sources import IDFSource
2223

2324
FORMAT_VERSION = '1.0.0'
@@ -36,11 +37,13 @@
3637
'source': Or(*[source.schema() for source in tools.sources.KNOWN_SOURCES]),
3738
'version': Or(*string_types),
3839
Optional('component_hash'): HASH_SCHEMA,
40+
Optional(str): object, # optional fields for future extensions
3941
}
4042
},
4143
'manifest_hash': HASH_SCHEMA,
4244
'version': And(Or(*string_types), len),
4345
Optional('target'): And(Use(str.lower), lambda s: s in known_targets()),
46+
Optional(str): object, # optional fields for future extensions
4447
}
4548
)
4649

@@ -104,10 +107,15 @@ def load(self): # type: () -> SolvedManifest
104107

105108
version = lock.pop('version')
106109
if version != FORMAT_VERSION:
107-
raise LockError(
108-
'Cannot parse components lock file.'
109-
'Lock file format version is %s, while only %s is supported'
110-
% (version, FORMAT_VERSION)
110+
notice(
111+
'Current idf-component-manager default lock file version is '
112+
'{}, '
113+
'but found {} in {}. '
114+
'Recreating lock file with the current version.'.format(
115+
FORMAT_VERSION,
116+
version,
117+
self._path,
118+
)
111119
)
112120

113121
return SolvedManifest.fromdict(lock)

idf_component_tools/manifest/solved_manifest.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
# SPDX-FileCopyrightText: 2022-2023 Espressif Systems (Shanghai) CO LTD
1+
# SPDX-FileCopyrightText: 2022-2024 Espressif Systems (Shanghai) CO LTD
22
# SPDX-License-Identifier: Apache-2.0
33

4+
from ..semver import Version
45
from .solved_component import SolvedComponent
56

67
try:
@@ -53,3 +54,10 @@ def serialize(self):
5354
@property
5455
def solved_components(self): # type: () -> dict[str, SolvedComponent]
5556
return {cmp.name: cmp for cmp in self.dependencies}
57+
58+
@property
59+
def idf_version(self): # type: () -> Version | None
60+
if 'idf' in self.solved_components:
61+
return self.solved_components['idf'].version.semver
62+
63+
return None
Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
1-
# SPDX-FileCopyrightText: 2023 Espressif Systems (Shanghai) CO LTD
1+
# SPDX-FileCopyrightText: 2023-2024 Espressif Systems (Shanghai) CO LTD
22
# SPDX-License-Identifier: Apache-2.0
33

44
WEB_SERVICE_REQUIRED_KEYS = {} # type: dict[str, str]
5-
WEB_SERVICE_OPTIONAL_KEYS = {'pre_release': 'bool', 'storage_url': 'str', 'service_url': 'str'}
5+
WEB_SERVICE_OPTIONAL_KEYS = {
6+
'pre_release': 'bool',
7+
'storage_url': 'str',
8+
'service_url': 'str',
9+
'registry_url': 'str',
10+
}

0 commit comments

Comments
 (0)