|
1 | 1 | import os |
2 | | -import pty |
3 | 2 | import re |
4 | 3 | import select |
5 | 4 | import shutil |
@@ -333,8 +332,8 @@ def _copy_and_build_source(self): |
333 | 332 | click.echo(click.style("Building frontend...", fg="blue")) |
334 | 333 | try: |
335 | 334 | # Run npm install and build |
336 | | - subprocess.run(["npm", "install"], cwd=self.project_root, check=True) |
337 | | - subprocess.run(["npm", "run", "build"], cwd=self.project_root, check=True) |
| 335 | + subprocess.run(["npm", "install"], cwd=self.project_root, check=True, shell=True) |
| 336 | + subprocess.run(["npm", "run", "build"], cwd=self.project_root, check=True, shell=True) |
338 | 337 |
|
339 | 338 | except subprocess.CalledProcessError as e: |
340 | 339 | click.echo(click.style(f"Error building frontend: {str(e)}", fg="red")) |
@@ -367,122 +366,173 @@ def _copy_and_build_source(self): |
367 | 366 |
|
368 | 367 | def _build_docker_image(self) -> str: |
369 | 368 | """ |
370 | | - Builds the Docker image using a pseudo-terminal (PTY) to preserve colored output. |
371 | | - Captures logs in plain text format (with ANSI codes removed) for cloud storage while |
372 | | - adding color to local terminal output for better readability. |
| 369 | + Builds the Docker image using a pseudo-terminal (PTY) to preserve colored output on Unix-like systems. |
| 370 | + On Windows, termios/pty is not available, so we fall back to a simpler subprocess approach. |
373 | 371 | """ |
374 | | - try: |
375 | | - docker_build_cmd = [ |
376 | | - "docker", |
377 | | - "build", |
378 | | - "--progress=plain", |
379 | | - # Simplifies logs by avoiding line overwrites in cloud logs; removes colors and animations |
380 | | - "-t", |
381 | | - self.image_name, |
382 | | - "-f", |
383 | | - self.dockerfile, |
384 | | - self.project_root, |
385 | | - ] |
386 | | - if self.no_cache: |
387 | | - docker_build_cmd.append("--no-cache") |
388 | | - |
389 | | - # Regex to strip ANSI escape sequences for storing logs as plain text |
390 | | - ansi_escape = re.compile(r"\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])") |
391 | | - |
392 | | - # Create a pseudo-terminal pair |
393 | | - master_fd, slave_fd = pty.openpty() |
394 | | - |
395 | | - # Spawn the Docker build process with PTY |
396 | | - process = subprocess.Popen( |
397 | | - docker_build_cmd, |
398 | | - stdin=slave_fd, |
399 | | - stdout=slave_fd, |
400 | | - stderr=slave_fd, |
401 | | - text=False, # Receive raw binary data (not decoded text) |
402 | | - bufsize=0, # No extra buffering |
| 372 | + docker_build_cmd = [ |
| 373 | + "docker", |
| 374 | + "build", |
| 375 | + "--progress=plain", |
| 376 | + "-t", |
| 377 | + self.image_name, |
| 378 | + "-f", |
| 379 | + self.dockerfile, |
| 380 | + self.project_root, |
| 381 | + ] |
| 382 | + if self.no_cache: |
| 383 | + docker_build_cmd.append("--no-cache") |
| 384 | + |
| 385 | + # Regex to strip ANSI escape sequences for storing logs as plain text |
| 386 | + ansi_escape = re.compile(r"\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])") |
| 387 | + |
| 388 | + if sys.platform == "win32": |
| 389 | + click.echo( |
| 390 | + click.style("Detected Windows: skipping PTY usage.", fg="yellow") |
403 | 391 | ) |
| 392 | + try: |
| 393 | + process = subprocess.Popen( |
| 394 | + docker_build_cmd, |
| 395 | + stdout=subprocess.PIPE, |
| 396 | + stderr=subprocess.PIPE, |
| 397 | + text=True, |
| 398 | + ) |
404 | 399 |
|
405 | | - # The slave FD is not needed after starting the process |
406 | | - os.close(slave_fd) |
407 | | - |
408 | | - build_logs = [] |
409 | | - |
410 | | - while True: |
411 | | - # Use select to check if there's data to read from the master FD |
412 | | - r, _, _ = select.select([master_fd], [], [], 0.1) |
413 | | - if master_fd in r: |
414 | | - # Read up to 1KB from the master FD |
415 | | - try: |
416 | | - chunk = os.read(master_fd, 1024) |
417 | | - except OSError: |
418 | | - # If reading fails, exit the loop |
419 | | - break |
| 400 | + build_logs = [] |
420 | 401 |
|
421 | | - if not chunk: |
422 | | - # EOF |
| 402 | + while True: |
| 403 | + line = process.stdout.readline() |
| 404 | + if not line and process.poll() is not None: |
423 | 405 | break |
424 | | - |
425 | | - # Remove ANSI codes and store logs for cloud storage |
426 | | - text_chunk = chunk.decode(errors="replace") |
427 | | - clean_text = ansi_escape.sub("", text_chunk) |
| 406 | + if line: |
| 407 | + clean_text = ansi_escape.sub("", line) |
| 408 | + build_logs.append(clean_text) |
| 409 | + # ローカル表示用にカラーをつけて出力 |
| 410 | + colored_chunk = click.style(clean_text, fg="blue") |
| 411 | + sys.stdout.write(colored_chunk) |
| 412 | + sys.stdout.flush() |
| 413 | + |
| 414 | + err_text = process.stderr.read() |
| 415 | + if err_text: |
| 416 | + clean_text = ansi_escape.sub("", err_text) |
428 | 417 | build_logs.append(clean_text) |
429 | | - |
430 | | - # Add color to logs for local terminal |
431 | 418 | colored_chunk = click.style(clean_text, fg="blue") |
432 | 419 | sys.stdout.write(colored_chunk) |
433 | 420 | sys.stdout.flush() |
434 | 421 |
|
435 | | - # If the process has exited, read any remaining data |
436 | | - if process.poll() is not None: |
437 | | - # Read everything left until EOF |
438 | | - while True: |
| 422 | + return_code = process.wait() |
| 423 | + if return_code != 0: |
| 424 | + all_logs = "".join(build_logs) |
| 425 | + raise subprocess.CalledProcessError( |
| 426 | + return_code, docker_build_cmd, output=all_logs |
| 427 | + ) |
| 428 | + |
| 429 | + click.echo( |
| 430 | + click.style( |
| 431 | + f"Docker image '{self.image_name}' built successfully.", |
| 432 | + fg="green", |
| 433 | + ) |
| 434 | + ) |
| 435 | + return "".join(build_logs) |
| 436 | + |
| 437 | + except subprocess.CalledProcessError as e: |
| 438 | + click.echo( |
| 439 | + click.style( |
| 440 | + f"Error building Docker image '{self.image_name}': {e.output}", |
| 441 | + fg="red", |
| 442 | + ) |
| 443 | + ) |
| 444 | + sys.exit(1) |
| 445 | + except Exception as e: |
| 446 | + click.echo( |
| 447 | + click.style( |
| 448 | + f"Unexpected error while building Docker image: {str(e)}", |
| 449 | + fg="red", |
| 450 | + ) |
| 451 | + ) |
| 452 | + sys.exit(1) |
| 453 | + |
| 454 | + else: |
| 455 | + try: |
| 456 | + import pty |
| 457 | + |
| 458 | + master_fd, slave_fd = pty.openpty() |
| 459 | + |
| 460 | + process = subprocess.Popen( |
| 461 | + docker_build_cmd, |
| 462 | + stdin=slave_fd, |
| 463 | + stdout=slave_fd, |
| 464 | + stderr=slave_fd, |
| 465 | + text=False, |
| 466 | + bufsize=0, |
| 467 | + ) |
| 468 | + |
| 469 | + os.close(slave_fd) |
| 470 | + |
| 471 | + build_logs = [] |
| 472 | + |
| 473 | + while True: |
| 474 | + r, _, _ = select.select([master_fd], [], [], 0.1) |
| 475 | + if master_fd in r: |
439 | 476 | try: |
440 | 477 | chunk = os.read(master_fd, 1024) |
441 | | - if not chunk: |
442 | | - break |
443 | | - text_chunk = chunk.decode(errors="replace") |
444 | | - clean_text = ansi_escape.sub("", text_chunk) |
445 | | - build_logs.append(clean_text) |
446 | | - colored_chunk = click.style(clean_text, fg="blue") |
447 | | - sys.stdout.write(colored_chunk) |
448 | | - sys.stdout.flush() |
449 | 478 | except OSError: |
450 | 479 | break |
451 | | - break |
452 | | - |
453 | | - # Close the master FD |
454 | | - os.close(master_fd) |
| 480 | + if not chunk: |
| 481 | + break |
| 482 | + text_chunk = chunk.decode(errors="replace") |
| 483 | + clean_text = ansi_escape.sub("", text_chunk) |
| 484 | + build_logs.append(clean_text) |
| 485 | + colored_chunk = click.style(clean_text, fg="blue") |
| 486 | + sys.stdout.write(colored_chunk) |
| 487 | + sys.stdout.flush() |
| 488 | + |
| 489 | + if process.poll() is not None: |
| 490 | + while True: |
| 491 | + try: |
| 492 | + chunk = os.read(master_fd, 1024) |
| 493 | + if not chunk: |
| 494 | + break |
| 495 | + text_chunk = chunk.decode(errors="replace") |
| 496 | + clean_text = ansi_escape.sub("", text_chunk) |
| 497 | + build_logs.append(clean_text) |
| 498 | + colored_chunk = click.style(clean_text, fg="blue") |
| 499 | + sys.stdout.write(colored_chunk) |
| 500 | + sys.stdout.flush() |
| 501 | + except OSError: |
| 502 | + break |
| 503 | + break |
455 | 504 |
|
456 | | - return_code = process.wait() |
457 | | - if return_code != 0: |
458 | | - # If Docker build failed, show the full logs and raise an error |
459 | | - all_logs = "".join(build_logs) |
460 | | - raise subprocess.CalledProcessError( |
461 | | - return_code, docker_build_cmd, output=all_logs |
462 | | - ) |
| 505 | + os.close(master_fd) |
| 506 | + return_code = process.wait() |
| 507 | + if return_code != 0: |
| 508 | + all_logs = "".join(build_logs) |
| 509 | + raise subprocess.CalledProcessError( |
| 510 | + return_code, docker_build_cmd, output=all_logs |
| 511 | + ) |
463 | 512 |
|
464 | | - click.echo( |
465 | | - click.style( |
466 | | - f"Docker image '{self.image_name}' built successfully.", fg="green" |
| 513 | + click.echo( |
| 514 | + click.style( |
| 515 | + f"Docker image '{self.image_name}' built successfully.", |
| 516 | + fg="green", |
| 517 | + ) |
467 | 518 | ) |
468 | | - ) |
469 | | - # Return the captured logs as plain text |
470 | | - return "".join(build_logs) |
| 519 | + return "".join(build_logs) |
471 | 520 |
|
472 | | - except subprocess.CalledProcessError: |
473 | | - click.echo( |
474 | | - click.style( |
475 | | - f"Error building Docker image '{self.image_name}'.", fg="red" |
| 521 | + except subprocess.CalledProcessError: |
| 522 | + click.echo( |
| 523 | + click.style( |
| 524 | + f"Error building Docker image '{self.image_name}'.", fg="red" |
| 525 | + ) |
476 | 526 | ) |
477 | | - ) |
478 | | - sys.exit(1) |
479 | | - except Exception as e: |
480 | | - click.echo( |
481 | | - click.style( |
482 | | - f"Unexpected error while building Docker image: {str(e)}", fg="red" |
| 527 | + sys.exit(1) |
| 528 | + except Exception as e: |
| 529 | + click.echo( |
| 530 | + click.style( |
| 531 | + f"Unexpected error while building Docker image: {str(e)}", |
| 532 | + fg="red", |
| 533 | + ) |
483 | 534 | ) |
484 | | - ) |
485 | | - sys.exit(1) |
| 535 | + sys.exit(1) |
486 | 536 |
|
487 | 537 | def _save_docker_image(self): |
488 | 538 | """ |
|
0 commit comments