Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 28 additions & 9 deletions cli/agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,11 @@ var agentChecksums = map[string]string{
// tvOS simulator runner is published from the same devicekit-ios release.
// Update this checksum whenever the tvOS runner artifact is rebuilt/republished.
"devicekit-tvos-Sim-arm64.zip": "49061f17046055c7e89dbf27067a59ab0bffd9d7d14d63031d5411c5963ccb81",
"mobilenext-devicekit.apk": "c58484b40db6ab84c7445ccaa346ee0804e155e37507a42f1ac37b92a52bdd4f",
// Real Apple TV runner IPA, re-signed at install time with a provisioning profile.
// Placeholder until the tvOS runner IPA is published from a devicekit-ios release;
// replace with the released artifact's SHA-256 when available.
"devicekit-tvos-runner.ipa": "0000000000000000000000000000000000000000000000000000000000000000",
"mobilenext-devicekit.apk": "c58484b40db6ab84c7445ccaa346ee0804e155e37507a42f1ac37b92a52bdd4f",
}

type agentMessageResponse struct {
Expand Down Expand Up @@ -139,8 +143,13 @@ var agentInstallCmd = &cobra.Command{
switch device.DeviceType() {
case "simulator":
installErr = installAgentOnSimulator(device)
case "real":
if agentProvisioningProfile == "" {
return fmt.Errorf("--provisioning-profile is required for real Apple TV devices")
}
installErr = installAgentOnRealTVOS(device)
default:
return fmt.Errorf("unsupported tvOS device type: %s (only the tvOS Simulator is supported)", device.DeviceType())
return fmt.Errorf("unsupported tvOS device type: %s", device.DeviceType())
}
case "android":
installErr = installAgentOnAndroid(device)
Expand Down Expand Up @@ -300,9 +309,11 @@ func installAgentOnSimulator(device devices.ControllableDevice) error {
return downloadAndInstallAgent(device, agentURL, filepath.Join(tmpDir, filename), nil)
}

func installAgentOnRealIOS(device devices.ControllableDevice) error {
filename := "devicekit-ios-runner.ipa"
agentURL := fmt.Sprintf("https://github.com/mobile-next/devicekit-ios/releases/download/%s/%s", agentVersionIOS, filename)
// installResignedRunner downloads a real-device runner IPA, re-signs it with the
// configured provisioning profile, and installs it. It backs both the iOS and the
// tvOS real-device flows, which differ only in the artifact filename and version.
func installResignedRunner(device devices.ControllableDevice, filename, version string) error {
agentURL := fmt.Sprintf("https://github.com/mobile-next/devicekit-ios/releases/download/%s/%s", version, filename)

tmpDir, err := os.MkdirTemp("", "mobilecli-agent-*")
if err != nil {
Expand All @@ -320,6 +331,14 @@ func installAgentOnRealIOS(device devices.ControllableDevice) error {
})
}

func installAgentOnRealIOS(device devices.ControllableDevice) error {
return installResignedRunner(device, "devicekit-ios-runner.ipa", agentVersionIOS)
}

func installAgentOnRealTVOS(device devices.ControllableDevice) error {
return installResignedRunner(device, "devicekit-tvos-runner.ipa", agentVersionTVOS)
}

func installAgentOnAndroid(device devices.ControllableDevice) error {
filename := "mobilenext-devicekit.apk"
agentURL := fmt.Sprintf("https://github.com/mobile-next/devicekit-android/releases/download/%s/%s", agentVersionAndroid, filename)
Expand Down Expand Up @@ -356,10 +375,10 @@ func findInstalledAgent(device devices.ControllableDevice) *devices.InstalledApp
}

// agentMatchesApp reports whether an installed app's bundle id identifies the agent.
// On iOS the runner bundle id can carry a signing/team prefix when re-signed, so a
// suffix match is used; other platforms require an exact match.
// On iOS and tvOS the runner bundle id can carry a signing/team prefix when re-signed
// for a real device, so a suffix match is used; other platforms require an exact match.
func agentMatchesApp(platform, installedPackage, agentPackage string) bool {
if platform == "ios" {
if platform == "ios" || platform == "tvos" {
return strings.HasSuffix(installedPackage, agentPackage)
}
return installedPackage == agentPackage
Expand Down Expand Up @@ -396,5 +415,5 @@ func init() {
agentStatusCmd.Flags().StringVar(&deviceId, "device", "", "ID of the device to check")
agentUninstallCmd.Flags().StringVar(&deviceId, "device", "", "ID of the device to uninstall the agent from")
agentInstallCmd.Flags().BoolVar(&agentForce, "force", false, "force install even if agent is already installed")
agentInstallCmd.Flags().StringVar(&agentProvisioningProfile, "provisioning-profile", "", "path to a .mobileprovision file to use for re-signing (required for real iOS devices)")
agentInstallCmd.Flags().StringVar(&agentProvisioningProfile, "provisioning-profile", "", "path to a .mobileprovision file to use for re-signing (required for real iOS and tvOS devices)")
}
57 changes: 57 additions & 0 deletions cli/agent_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package cli

import "testing"

func TestAgentPackageForPlatform(t *testing.T) {
cases := map[string]string{
"ios": iosRunnerBundleID,
"tvos": tvosRunnerBundleID,
"android": androidPackageName,
"unknown": "",
}
for platform, want := range cases {
if got := agentPackageForPlatform(platform); got != want {
t.Errorf("agentPackageForPlatform(%q) = %q, want %q", platform, got, want)
}
}
}

func TestAgentVersionForPlatform(t *testing.T) {
cases := map[string]string{
"ios": agentVersionIOS,
"tvos": agentVersionTVOS,
"android": agentVersionAndroid,
"unknown": "",
}
for platform, want := range cases {
if got := agentVersionForPlatform(platform); got != want {
t.Errorf("agentVersionForPlatform(%q) = %q, want %q", platform, got, want)
}
}
}

func TestAgentMatchesApp(t *testing.T) {
// Re-signing a runner for a real device prefixes the bundle id with the team id,
// so iOS and tvOS must match on suffix while other platforms match exactly.
cases := []struct {
name string
platform string
installedPackage string
agentPackage string
want bool
}{
{"ios exact", "ios", iosRunnerBundleID, iosRunnerBundleID, true},
{"ios team-prefixed", "ios", "ABCDE12345." + iosRunnerBundleID, iosRunnerBundleID, true},
{"tvos exact", "tvos", tvosRunnerBundleID, tvosRunnerBundleID, true},
{"tvos team-prefixed", "tvos", "ABCDE12345." + tvosRunnerBundleID, tvosRunnerBundleID, true},
{"tvos mismatch", "tvos", "com.example.other", tvosRunnerBundleID, false},
{"android exact", "android", androidPackageName, androidPackageName, true},
{"android no suffix match", "android", "prefix." + androidPackageName, androidPackageName, false},
}
for _, c := range cases {
if got := agentMatchesApp(c.platform, c.installedPackage, c.agentPackage); got != c.want {
t.Errorf("%s: agentMatchesApp(%q, %q, %q) = %v, want %v",
c.name, c.platform, c.installedPackage, c.agentPackage, got, c.want)
}
}
}
7 changes: 7 additions & 0 deletions devices/ios.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,14 @@ func (d IOSDevice) Version() string {
return d.OSVersion
}

// Platform reports the OS family of the connected device. Real Apple TV units are
// discovered over the same usbmuxd/go-ios path as iPhones and iPads, so they are
// distinguished by their product type (e.g. "AppleTV14,1") rather than a separate
// device class.
func (d IOSDevice) Platform() string {
if strings.HasPrefix(d.ProductType, "AppleTV") {
return "tvos"
}
return "ios"
}

Expand Down
31 changes: 31 additions & 0 deletions devices/ios_platform_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package devices

import "testing"

func TestIOSDevicePlatform(t *testing.T) {
// Real Apple TV units are discovered over the same go-ios path as iPhones/iPads
// and are identified by their product type rather than a separate device class.
cases := []struct {
productType string
want string
}{
{"iPhone15,3", "ios"},
{"iPad13,1", "ios"},
{"AppleTV14,1", "tvos"},
{"AppleTV11,1", "tvos"},
{"", "ios"},
}
for _, c := range cases {
d := IOSDevice{ProductType: c.productType}
if got := d.Platform(); got != c.want {
t.Errorf("IOSDevice{ProductType: %q}.Platform() = %q, want %q", c.productType, got, c.want)
}
}
}

func TestIOSDeviceType(t *testing.T) {
d := IOSDevice{ProductType: "AppleTV14,1"}
if got := d.DeviceType(); got != "real" {
t.Errorf("IOSDevice.DeviceType() = %q, want %q", got, "real")
}
}