Skip to content

Commit

Permalink
Install Rosetta if necessary
Browse files Browse the repository at this point in the history
  • Loading branch information
mhsmith committed Dec 14, 2022
1 parent 76ebb8e commit 259aec2
Show file tree
Hide file tree
Showing 3 changed files with 161 additions and 23 deletions.
1 change: 1 addition & 0 deletions changes/1000.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
On MacOS, Rosetta is now installed automatically if needed.
25 changes: 25 additions & 0 deletions src/briefcase/integrations/java.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,10 @@ def verify(cls, tools: ToolCache, install=True):
java_home = tools.os.environ.get("JAVA_HOME", "")
install_message = None

if tools.host_arch == "arm64" and tools.host_os == "Darwin":
# Java 8 is not available for MacOS on ARM64, so we will require Rosetta.
cls.verify_rosetta(tools)

# macOS has a helpful system utility to determine JAVA_HOME. Try it.
if not java_home and tools.host_os == "Darwin":
try:
Expand Down Expand Up @@ -284,3 +288,24 @@ def upgrade(self):

self.uninstall()
self.install()

@classmethod
def verify_rosetta(cls, tools):
try:
tools.subprocess.check_output(
["arch", "-x86_64", "true"], stderr=subprocess.STDOUT
)
except subprocess.CalledProcessError:
tools.logger.info(
"""\
This command requires Rosetta, but it does not appear to be installed. Briefcase will
attempt to install it now.
"""
)
try:
tools.subprocess.run(
["softwareupdate", "--install-rosetta", "--agree-to-license"],
check=True,
)
except subprocess.CalledProcessError as e:
raise BriefcaseCommandError("Failed to install Rosetta") from e
158 changes: 135 additions & 23 deletions tests/integrations/java/test_JDK__verify.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,20 @@
import shutil
import subprocess
from pathlib import Path
from subprocess import CalledProcessError
from unittest import mock

import pytest

from briefcase.exceptions import BriefcaseCommandError, MissingToolError, NetworkFailure
from briefcase.integrations.java import JDK

CALL_JAVA_HOME = mock.call(["/usr/libexec/java_home"], stderr=subprocess.STDOUT)
CALL_ROSETTA_CHECK = mock.call(["arch", "-x86_64", "true"], stderr=subprocess.STDOUT)
CALL_ROSETTA_INSTALL = mock.call(
["softwareupdate", "--install-rosetta", "--agree-to-license"], check=True
)


def test_short_circuit(mock_tools):
"""Tool is not created if already cached."""
Expand All @@ -25,6 +32,9 @@ def test_macos_tool_java_home(mock_tools, capsys):
# Mock being on macOS
mock_tools.host_os = "Darwin"

# Prevent Rosetta check.
mock_tools.host_arch = "x86_64"

# Mock 2 calls to check_output.
mock_tools.subprocess.check_output.side_effect = [
"/path/to/java",
Expand All @@ -37,20 +47,14 @@ def test_macos_tool_java_home(mock_tools, capsys):
# The JDK should have the path returned by the tool
assert mock_tools.java.java_home == Path("/path/to/java")

mock_tools.subprocess.check_output.assert_has_calls(
[
# First call is to /usr/lib/java_home
mock.call(
["/usr/libexec/java_home"],
stderr=subprocess.STDOUT,
),
# Second is a call to verify a valid Java version
mock.call(
[os.fsdecode(Path("/path/to/java/bin/javac")), "-version"],
stderr=subprocess.STDOUT,
),
]
)
assert mock_tools.subprocess.check_output.mock_calls == [
CALL_JAVA_HOME,
# Second is a call to verify a valid Java version
mock.call(
[os.fsdecode(Path("/path/to/java/bin/javac")), "-version"],
stderr=subprocess.STDOUT,
),
]

# No console output
output = capsys.readouterr()
Expand All @@ -63,6 +67,9 @@ def test_macos_tool_failure(mock_tools, tmp_path, capsys):
# Mock being on macOS
mock_tools.host_os = "Darwin"

# Prevent Rosetta check.
mock_tools.host_arch = "x86_64"

# Mock a failed call on the libexec tool
mock_tools.subprocess.check_output.side_effect = subprocess.CalledProcessError(
returncode=1, cmd="/usr/libexec/java_home"
Expand All @@ -77,15 +84,7 @@ def test_macos_tool_failure(mock_tools, tmp_path, capsys):
# The JDK should have the briefcase JAVA_HOME
assert jdk.java_home == tmp_path / "tools" / "java" / "Contents" / "Home"

mock_tools.subprocess.check_output.assert_has_calls(
[
# First call is to /usr/lib/java_home
mock.call(
["/usr/libexec/java_home"],
stderr=subprocess.STDOUT,
),
]
)
assert mock_tools.subprocess.check_output.mock_calls == [CALL_JAVA_HOME]

# No console output
output = capsys.readouterr()
Expand All @@ -98,6 +97,9 @@ def test_macos_provided_overrides_tool_java_home(mock_tools, capsys):
# Mock being on macOS
mock_tools.host_os = "Darwin"

# Prevent Rosetta check.
mock_tools.host_arch = "x86_64"

# Setup explicit JAVA_HOME
mock_tools.os.environ = {"JAVA_HOME": "/path/to/java"}

Expand All @@ -124,6 +126,9 @@ def test_macos_provided_overrides_tool_java_home(mock_tools, capsys):

def test_valid_provided_java_home(mock_tools, capsys):
"""If a valid JAVA_HOME is provided, it is used."""
# Prevent Rosetta check.
mock_tools.host_arch = "x86_64"

# Setup explicit JAVA_HOME
mock_tools.os.environ = {"JAVA_HOME": "/path/to/java"}

Expand Down Expand Up @@ -162,6 +167,9 @@ def test_invalid_jdk_version(mock_tools, host_os, java_home, tmp_path, capsys):
# Mock os
mock_tools.host_os = host_os

# Prevent Rosetta check.
mock_tools.host_arch = "x86_64"

# Setup explicit JAVA_HOME
mock_tools.os.environ = {"JAVA_HOME": "/path/to/java"}

Expand Down Expand Up @@ -203,6 +211,9 @@ def test_no_javac(mock_tools, host_os, java_home, tmp_path, capsys):
# Mock os
mock_tools.host_os = host_os

# Prevent Rosetta check.
mock_tools.host_arch = "x86_64"

# Setup explicit JAVA_HOME
mock_tools.os.environ = {"JAVA_HOME": "/path/to/nowhere"}

Expand Down Expand Up @@ -243,6 +254,9 @@ def test_javac_error(mock_tools, host_os, java_home, tmp_path, capsys):
# Mock os
mock_tools.host_os = host_os

# Prevent Rosetta check.
mock_tools.host_arch = "x86_64"

# Setup explicit JAVA_HOME
mock_tools.os.environ = {"JAVA_HOME": "/path/to/nowhere"}

Expand Down Expand Up @@ -285,6 +299,9 @@ def test_unparseable_javac_version(mock_tools, host_os, java_home, tmp_path, cap
# Mock os
mock_tools.host_os = host_os

# Prevent Rosetta check.
mock_tools.host_arch = "x86_64"

# Setup explicit JAVA_HOME
mock_tools.os.environ = {"JAVA_HOME": "/path/to/nowhere"}

Expand Down Expand Up @@ -455,3 +472,98 @@ def test_invalid_jdk_archive(mock_tools, tmp_path):
)
# The original archive was not deleted
assert archive.unlink.call_count == 0


def test_rosetta_host_os(mock_tools, tmp_path):
"""On an OS other than macOS, the Rosetta check does not occur."""
mock_tools.host_os = "Linux"
mock_tools.host_arch = "arm64"

# Create a mock of a previously installed Java version.
(tmp_path / "tools" / "java" / "bin").mkdir(parents=True)

JDK.verify(mock_tools)
mock_tools.subprocess.check_output.assert_not_called()
mock_tools.subprocess.run.assert_not_called()


def test_rosetta_host_arch(mock_tools, tmp_path):
"""On an architecture other than ARM64, the Rosetta check does not
occur."""
mock_tools.host_os = "Darwin"
mock_tools.host_arch = "x86_64"

mock_tools.subprocess.check_output.side_effect = [
CalledProcessError(1, "java_home")
]

# Create a mock of a previously installed Java version.
(tmp_path / "tools" / "java" / "Contents" / "Home" / "bin").mkdir(parents=True)

JDK.verify(mock_tools)
assert mock_tools.subprocess.check_output.mock_calls == [CALL_JAVA_HOME]
mock_tools.subprocess.run.assert_not_called()


def test_rosetta_already_installed(mock_tools, tmp_path):
"""On an ARM Mac, the Rosetta check occurs before calling any other Java
commands."""
mock_tools.host_os = "Darwin"
mock_tools.host_arch = "arm64"

mock_tools.subprocess.check_output.side_effect = [
None, # Rosetta check succeeds.
CalledProcessError(1, "java_home"),
]

# Create a mock of a previously installed Java version.
(tmp_path / "tools" / "java" / "Contents" / "Home" / "bin").mkdir(parents=True)

JDK.verify(mock_tools)
assert mock_tools.subprocess.check_output.mock_calls == [
CALL_ROSETTA_CHECK,
CALL_JAVA_HOME,
]
mock_tools.subprocess.run.assert_not_called()


def test_rosetta_install_success(mock_tools, tmp_path):
"""Rosetta is installed if necessary."""
mock_tools.host_os = "Darwin"
mock_tools.host_arch = "arm64"

mock_tools.subprocess.check_output.side_effect = [
CalledProcessError(1, "arch"),
CalledProcessError(1, "java_home"),
]

# Create a mock of a previously installed Java version.
(tmp_path / "tools" / "java" / "Contents" / "Home" / "bin").mkdir(parents=True)

JDK.verify(mock_tools)
assert mock_tools.subprocess.check_output.mock_calls == [
CALL_ROSETTA_CHECK,
CALL_JAVA_HOME,
]
assert mock_tools.subprocess.run.mock_calls == [CALL_ROSETTA_INSTALL]


def test_rosetta_install_failure(mock_tools, tmp_path):
"""If Rosetta install fails, no Java commands are called."""
mock_tools.host_os = "Darwin"
mock_tools.host_arch = "arm64"

mock_tools.subprocess.check_output.side_effect = [
CalledProcessError(1, "arch"),
]
mock_tools.subprocess.run.side_effect = [
CalledProcessError(1, "softwareupdate"),
]

# Create a mock of a previously installed Java version.
(tmp_path / "tools" / "java" / "Contents" / "Home" / "bin").mkdir(parents=True)

with pytest.raises(BriefcaseCommandError, match="Failed to install Rosetta"):
JDK.verify(mock_tools)
assert mock_tools.subprocess.check_output.mock_calls == [CALL_ROSETTA_CHECK]
assert mock_tools.subprocess.run.mock_calls == [CALL_ROSETTA_INSTALL]

0 comments on commit 259aec2

Please sign in to comment.