|
5 | 5 | import sys
|
6 | 6 | from subprocess import run
|
7 | 7 |
|
| 8 | +from filelock import FileLock |
| 9 | + |
8 | 10 | from .compat import Compat, Version
|
9 | 11 | from .find_julia import find_julia, julia_version
|
10 | 12 | from .install_julia import log
|
@@ -287,94 +289,117 @@ def merge_any(dep, kfvs, k):
|
287 | 289 |
|
288 | 290 |
|
289 | 291 | def resolve(force=False, dry_run=False):
|
290 |
| - # see if we can skip resolving |
291 |
| - if not force: |
292 |
| - if STATE["resolved"]: |
293 |
| - return True |
294 |
| - deps = can_skip_resolve() |
295 |
| - if deps: |
296 |
| - STATE["resolved"] = True |
297 |
| - STATE["executable"] = deps["executable"] |
298 |
| - STATE["version"] = Version.parse(deps["version"]) |
299 |
| - return True |
300 |
| - if dry_run: |
| 292 | + # fast check to see if we have already resolved |
| 293 | + if (not force) and STATE["resolved"]: |
301 | 294 | return False
|
302 | 295 | STATE["resolved"] = False
|
303 |
| - # get julia compat and required packages |
304 |
| - compat, pkgs = find_requirements() |
305 |
| - # find a compatible julia executable |
306 |
| - log(f'Locating Julia{"" if compat is None else " "+str(compat)}') |
307 |
| - exe, ver = find_julia( |
308 |
| - compat=compat, prefix=STATE["install"], install=True, upgrade=True |
309 |
| - ) |
310 |
| - log(f"Using Julia {ver} at {exe}") |
311 |
| - # set up the project |
| 296 | + # use a lock to prevent concurrent resolution |
312 | 297 | project = STATE["project"]
|
313 |
| - log(f"Using Julia project at {project}") |
314 | 298 | os.makedirs(project, exist_ok=True)
|
315 |
| - if not STATE["offline"]: |
316 |
| - # write a Project.toml specifying UUIDs and compatibility of required packages |
317 |
| - with open(os.path.join(project, "Project.toml"), "wt") as fp: |
318 |
| - print("[deps]", file=fp) |
319 |
| - for pkg in pkgs: |
320 |
| - print(f'{pkg.name} = "{pkg.uuid}"', file=fp) |
321 |
| - print(file=fp) |
322 |
| - print("[compat]", file=fp) |
323 |
| - for pkg in pkgs: |
324 |
| - if pkg.version: |
325 |
| - print(f'{pkg.name} = "{pkg.version}"', file=fp) |
326 |
| - print(file=fp) |
327 |
| - # remove Manifest.toml |
328 |
| - manifest_path = os.path.join(project, "Manifest.toml") |
329 |
| - if os.path.exists(manifest_path): |
330 |
| - os.remove(manifest_path) |
331 |
| - # install the packages |
332 |
| - dev_pkgs = ", ".join([pkg.jlstr() for pkg in pkgs if pkg.dev]) |
333 |
| - add_pkgs = ", ".join([pkg.jlstr() for pkg in pkgs if not pkg.dev]) |
334 |
| - script = ["import Pkg", "Pkg.Registry.update()"] |
335 |
| - if dev_pkgs: |
336 |
| - script.append(f"Pkg.develop([{dev_pkgs}])") |
337 |
| - if add_pkgs: |
338 |
| - script.append(f"Pkg.add([{add_pkgs}])") |
339 |
| - script.append("Pkg.resolve()") |
340 |
| - script.append("Pkg.precompile()") |
341 |
| - log("Installing packages:") |
342 |
| - for line in script: |
343 |
| - log("julia>", line, cont=True) |
344 |
| - env = os.environ.copy() |
345 |
| - if sys.executable: |
346 |
| - # prefer PythonCall to use the current Python executable |
347 |
| - # TODO: this is a hack, it would be better for PythonCall to detect that |
348 |
| - # Julia is being called from Python |
349 |
| - env.setdefault("JULIA_PYTHONCALL_EXE", sys.executable) |
350 |
| - run( |
351 |
| - [exe, "--project=" + project, "--startup-file=no", "-e", "; ".join(script)], |
352 |
| - check=True, |
353 |
| - env=env, |
| 299 | + lock_file = os.path.join(project, "lock.pid") |
| 300 | + lock = FileLock(lock_file) |
| 301 | + try: |
| 302 | + lock.acquire(timeout=3) |
| 303 | + except TimeoutError: |
| 304 | + log( |
| 305 | + f"Waiting for lock on {lock_file} to be freed. This normally means that" |
| 306 | + " another process is resolving. If you know that no other process is" |
| 307 | + " resolving, delete this file to proceed." |
354 | 308 | )
|
355 |
| - # record that we resolved |
356 |
| - save_meta( |
357 |
| - { |
358 |
| - "meta_version": META_VERSION, |
359 |
| - "dev": STATE["dev"], |
360 |
| - "version": str(ver), |
361 |
| - "executable": exe, |
362 |
| - "deps_files": { |
363 |
| - filename: { |
364 |
| - "timestamp": os.path.getmtime(filename), |
365 |
| - "hash_sha256": _get_hash(filename), |
366 |
| - } |
367 |
| - for filename in deps_files() |
368 |
| - }, |
369 |
| - "pkgs": [pkg.dict() for pkg in pkgs], |
370 |
| - "offline": bool(STATE["offline"]), |
371 |
| - "override_executable": STATE["override_executable"], |
372 |
| - } |
373 |
| - ) |
374 |
| - STATE["resolved"] = True |
375 |
| - STATE["executable"] = exe |
376 |
| - STATE["version"] = ver |
377 |
| - return True |
| 309 | + lock.acquire() |
| 310 | + try: |
| 311 | + # see if we can skip resolving |
| 312 | + if not force: |
| 313 | + deps = can_skip_resolve() |
| 314 | + if deps: |
| 315 | + STATE["resolved"] = True |
| 316 | + STATE["executable"] = deps["executable"] |
| 317 | + STATE["version"] = Version.parse(deps["version"]) |
| 318 | + return True |
| 319 | + if dry_run: |
| 320 | + return False |
| 321 | + # get julia compat and required packages |
| 322 | + compat, pkgs = find_requirements() |
| 323 | + # find a compatible julia executable |
| 324 | + log(f"Locating Julia{'' if compat is None else ' ' + str(compat)}") |
| 325 | + exe, ver = find_julia( |
| 326 | + compat=compat, prefix=STATE["install"], install=True, upgrade=True |
| 327 | + ) |
| 328 | + log(f"Using Julia {ver} at {exe}") |
| 329 | + # set up the project |
| 330 | + log(f"Using Julia project at {project}") |
| 331 | + if not STATE["offline"]: |
| 332 | + # write a Project.toml specifying UUIDs and compatibility of required |
| 333 | + # packages |
| 334 | + with open(os.path.join(project, "Project.toml"), "wt") as fp: |
| 335 | + print("[deps]", file=fp) |
| 336 | + for pkg in pkgs: |
| 337 | + print(f'{pkg.name} = "{pkg.uuid}"', file=fp) |
| 338 | + print(file=fp) |
| 339 | + print("[compat]", file=fp) |
| 340 | + for pkg in pkgs: |
| 341 | + if pkg.version: |
| 342 | + print(f'{pkg.name} = "{pkg.version}"', file=fp) |
| 343 | + print(file=fp) |
| 344 | + # remove Manifest.toml |
| 345 | + manifest_path = os.path.join(project, "Manifest.toml") |
| 346 | + if os.path.exists(manifest_path): |
| 347 | + os.remove(manifest_path) |
| 348 | + # install the packages |
| 349 | + dev_pkgs = ", ".join([pkg.jlstr() for pkg in pkgs if pkg.dev]) |
| 350 | + add_pkgs = ", ".join([pkg.jlstr() for pkg in pkgs if not pkg.dev]) |
| 351 | + script = ["import Pkg", "Pkg.Registry.update()"] |
| 352 | + if dev_pkgs: |
| 353 | + script.append(f"Pkg.develop([{dev_pkgs}])") |
| 354 | + if add_pkgs: |
| 355 | + script.append(f"Pkg.add([{add_pkgs}])") |
| 356 | + script.append("Pkg.resolve()") |
| 357 | + script.append("Pkg.precompile()") |
| 358 | + log("Installing packages:") |
| 359 | + for line in script: |
| 360 | + log("julia>", line, cont=True) |
| 361 | + env = os.environ.copy() |
| 362 | + if sys.executable: |
| 363 | + # prefer PythonCall to use the current Python executable |
| 364 | + # TODO: this is a hack, it would be better for PythonCall to detect that |
| 365 | + # Julia is being called from Python |
| 366 | + env.setdefault("JULIA_PYTHONCALL_EXE", sys.executable) |
| 367 | + run( |
| 368 | + [ |
| 369 | + exe, |
| 370 | + "--project=" + project, |
| 371 | + "--startup-file=no", |
| 372 | + "-e", |
| 373 | + "; ".join(script), |
| 374 | + ], |
| 375 | + check=True, |
| 376 | + env=env, |
| 377 | + ) |
| 378 | + # record that we resolved |
| 379 | + save_meta( |
| 380 | + { |
| 381 | + "meta_version": META_VERSION, |
| 382 | + "dev": STATE["dev"], |
| 383 | + "version": str(ver), |
| 384 | + "executable": exe, |
| 385 | + "deps_files": { |
| 386 | + filename: { |
| 387 | + "timestamp": os.path.getmtime(filename), |
| 388 | + "hash_sha256": _get_hash(filename), |
| 389 | + } |
| 390 | + for filename in deps_files() |
| 391 | + }, |
| 392 | + "pkgs": [pkg.dict() for pkg in pkgs], |
| 393 | + "offline": bool(STATE["offline"]), |
| 394 | + "override_executable": STATE["override_executable"], |
| 395 | + } |
| 396 | + ) |
| 397 | + STATE["resolved"] = True |
| 398 | + STATE["executable"] = exe |
| 399 | + STATE["version"] = ver |
| 400 | + return True |
| 401 | + finally: |
| 402 | + lock.release() |
378 | 403 |
|
379 | 404 |
|
380 | 405 | def executable():
|
|
0 commit comments