Skip to content

Commit c7a0b32

Browse files
authored
Merge pull request #123 from morph-data/fix/windows-deploy
Fix/windows deploy
2 parents 9dbe52d + ee277d0 commit c7a0b32

2 files changed

Lines changed: 150 additions & 100 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ Understanding the concept of developing an AI app in Morph will let you do a fly
5353

5454
1. Create each files in `python` and `pages` directories.
5555

56-
Python: Using Plotly to create a AI workflow.
56+
Python: Using Langchain to create a AI workflow.
5757

5858
```python
5959
import morph

core/morph/task/deploy.py

Lines changed: 149 additions & 99 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import os
2-
import pty
32
import re
43
import select
54
import shutil
@@ -333,8 +332,8 @@ def _copy_and_build_source(self):
333332
click.echo(click.style("Building frontend...", fg="blue"))
334333
try:
335334
# 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)
338337

339338
except subprocess.CalledProcessError as e:
340339
click.echo(click.style(f"Error building frontend: {str(e)}", fg="red"))
@@ -367,122 +366,173 @@ def _copy_and_build_source(self):
367366

368367
def _build_docker_image(self) -> str:
369368
"""
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.
373371
"""
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")
403391
)
392+
try:
393+
process = subprocess.Popen(
394+
docker_build_cmd,
395+
stdout=subprocess.PIPE,
396+
stderr=subprocess.PIPE,
397+
text=True,
398+
)
404399

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 = []
420401

421-
if not chunk:
422-
# EOF
402+
while True:
403+
line = process.stdout.readline()
404+
if not line and process.poll() is not None:
423405
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)
428417
build_logs.append(clean_text)
429-
430-
# Add color to logs for local terminal
431418
colored_chunk = click.style(clean_text, fg="blue")
432419
sys.stdout.write(colored_chunk)
433420
sys.stdout.flush()
434421

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:
439476
try:
440477
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()
449478
except OSError:
450479
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
455504

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+
)
463512

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+
)
467518
)
468-
)
469-
# Return the captured logs as plain text
470-
return "".join(build_logs)
519+
return "".join(build_logs)
471520

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+
)
476526
)
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+
)
483534
)
484-
)
485-
sys.exit(1)
535+
sys.exit(1)
486536

487537
def _save_docker_image(self):
488538
"""

0 commit comments

Comments
 (0)