def validate_app(platforms): push_successful = push(track_event=False) if not push_successful: # The push failed, so no point in validating the app sys.exit(1) # Ensure that a publish directory exists ensure_publish_dir() ensure_platform_keys() # Load the app config and wrapper auth = Auth() conf = Config() siphon = Siphon(auth.auth_token) # Send a request to out validation endpoint to make sure we # have the required details for p in platforms: if p == 'ios': formatted_platform = 'iOS' else: formatted_platform = 'Android' puts(colored.yellow('Validating your app for %s publishing...' % \ formatted_platform)) status = siphon.validate_app(conf.app_id, p) if status == 'ok': puts(colored.green('OK.')) else: return False return True
def ensure_base(version): # Make sure that the correct siphon-base package is installed for this # base version; if not, install it. Returns the location of the # base project. auth = Auth() conf = Config() siphon = Siphon(auth.auth_token) build = Build(conf.app_id, version) pkg_url = siphon.base_package_url(version) pkg_dest = Cache.base_package_path(version) puts(colored.yellow('Checking for required packages...')) content_length = siphon.content_length_for_version(version) if Cache.base_package_installed(version): cached_content_length = Cache.get_length_for_base_version(version) if content_length != cached_content_length: update_msg = 'Siphon needs to update some files in order to run ' \ 'your app (%s). Proceed? ' \ '[Y/n]: ' % format_size(content_length) update = yn(update_msg) if not update: sys.exit(1) msg = 'Updating Siphon files for base version %s...' % version try: download_file(pkg_url, pkg_dest, msg) Cache.set_length_for_base_version(version, content_length) except KeyboardInterrupt: sys.exit(1) # Get rid of the old build dirs (they are now invalid) build.clean_builds() else: download_msg = 'Siphon needs to download some files in order to run ' \ 'your app (%s). This will not be ' \ 'required again unless an update is needed, or a ' \ 'different base version is specified in ' \ 'the Siphonfile of one of your apps. Proceed? ' \ '[Y/n]: ' % format_size(content_length) download = yn(download_msg) if not download: sys.exit(1) msg = 'Downloading compatibility files for base version ' \ '%s...' % version try: download_file(pkg_url, pkg_dest, msg) except KeyboardInterrupt: sys.exit(1) Cache.set_length_for_base_version(version, content_length) build.clean_builds() build.ensure_build_dir()
def share_with_team_member(email): auth = Auth() siphon = Siphon(auth.auth_token) conf = Config() # Prompt the user with implications before we do anything. puts(colored.yellow('Checking the sharing status for this app...')) sharing_status = siphon.get_sharing_status(conf.app_id) shared_with = sharing_status.get('shared_with', []) if not prompt_team_share(shared_with): return siphon.share(conf.app_id, 'team-member', email) puts(colored.green('Your app was shared successfully.')) mixpanel_event(MIXPANEL_SHARE_TEAM_MEMBER)
def push(track_event=True, watch=False, app_id=None): # Request the correct bundler URL from siphon-web puts(colored.green('Preparing to push your local files...')) conf = Config() auth = Auth() siphon = Siphon(auth.auth_token) if app_id is None: app_id = conf.app_id if track_event: event_properties = {'app_id': app_id, 'watch': watch} mixpanel_event(MIXPANEL_EVENT_PUSH, properties=event_properties) bundler_url = siphon.get_bundler_url(app_id, action='push') server_hashes = get_hashes(bundler_url) return post_archive(bundler_url, server_hashes)
def stream_logs(): conf = Config() # Request the correct streamer URL from siphon-web auth = Auth() siphon = Siphon(auth.auth_token) # Track mixpanel_event(MIXPANEL_EVENT_LOGS, properties={'app_id': conf.app_id}) streamer_url = siphon.get_streamer_url(conf.app_id, 'log_reader') puts(colored.yellow('Connecting...')) ws = websocket.create_connection(streamer_url) puts(colored.green('Streaming logs and errors... (ctrl-c to stop)\n')) try: for line in ws: print(line) except KeyboardInterrupt: puts(colored.yellow('\nClosing the connection.')) ws.close()
def share_with_beta_tester(email=None): auth = Auth() siphon = Siphon(auth.auth_token) conf = Config() # Prompt the user with implications before we do anything. puts(colored.yellow('Checking the sharing status for this app...')) sharing_status = siphon.get_sharing_status(conf.app_id) shared_with = sharing_status.get('shared_with', []) if email: if not prompt_beta_share_for_specific_email(shared_with): return else: if len(shared_with) < 1: msg = 'Running the "siphon share" command without specifying ' \ 'an email address is only valid if you have one-or-more ' \ 'beta testers who already accepted an invitation.' puts(colored.red(msg)) return if not prompt_beta_share_for_all(shared_with): return # Do a push to the *aliased* beta testing app. puts(colored.green('Pushing your local files for beta testing...')) aliased_app_id = sharing_status['aliased_app']['id'] if not push(app_id=aliased_app_id, track_event=False): puts(colored.red('\nThe push failed so your beta testers were not ' \ 'notified.')) return # We only need to add a sharing permission if an email was specified, # because the push itself triggers notifications. if email is not None: siphon.share(conf.app_id, 'beta-tester', email) puts(colored.green('\nYour app was shared successfully.')) mixpanel_event(MIXPANEL_SHARE_BETA_TESTER)
def print_apps(): auth = Auth() mixpanel_event(MIXPANEL_EVENT_LIST) siphon = Siphon(auth.auth_token) for app in siphon.list_apps(): print(app['name'])
def publish(platform): # Ensure that a publish directory exists ensure_publish_dir() ensure_platform_keys() # Load the app config and wrapper auth = Auth() conf = Config() siphon = Siphon(auth.auth_token) mixpanel_props = {'upgrade_required': False, 'platform': platform} # We first check that the user has a valid subscription, because we're # going to do an implicit push before submitting, and it would otherwise # be confusing. puts(colored.yellow('Checking your account...')) if not siphon.user_can_publish(): mixpanel_props['upgrade_required'] = True mixpanel_event(MIXPANEL_EVENT_PUBLISH, properties=mixpanel_props) prompt_for_upgrade() sys.exit(1) else: puts(colored.green('Your account is ready for publishing.')) mixpanel_event(MIXPANEL_EVENT_PUBLISH, properties=mixpanel_props) # It looks like the user can publish, so we do an implicit push # and validate the app. app_valid = validate_app([platform]) if not app_valid: # The last push before publishing should be a successful one sys.exit(1) # Check if this submission requires a hard update, and if so then # prompt the user before going ahead with the submit so they understand. hard_update = siphon.app_requires_hard_update(conf.app_id, platform) if hard_update: if not prompt_for_hard_update(platform): return else: puts(colored.green('This update can be published without changing ' \ 'the app binary. It will be available straight away.')) # Prompt for platform info and do the actual submission user, password = None, None if hard_update: user, password = prompt_for_platform_info(platform) if hard_update: if not yn('Your app is about to be processed and submitted to the ' \ 'store. Are you sure you want to continue? [Y/n]: '): sys.exit(0) else: if not yn('Your app update is about to be submitted and the users ' \ 'of your app will receive it as an over-the-air update. Are you ' \ 'sure you want to continue? [Y/n]: '): sys.exit(0) puts(colored.yellow('Submitting...')) siphon.submit(conf.app_id, platform, username=user, password=password) puts(colored.green('\nThanks! Your app is in the queue. We will send ' \ 'status updates to your registered email address.')) print('Please note that after this submission has been released, you ' \ 'can push instant updates to your users by running the ' \ '"siphon publish" command again.')
def play(device, dev_mode=False, platform='ios'): """ Takes a device object and runs an app """ conf = Config() auth = Auth() siphon = Siphon(auth.auth_token) # Mixpanel tracking current_app_data = siphon.get_app(conf.app_id) mixpanel_props = { 'app_id': conf.app_id, 'app_name': current_app_data['name'], } mixpanel_event(MIXPANEL_EVENT_PLAY, properties=mixpanel_props) # The app needs to be built (archived) if: # # 1. An archive for this base version doesn't exist # 2. An archive exists and the display name has changed # 3. An archive exists and the facebook app id has changed # 4. An archive exists and the provisioning profile used to create it # is not installed or is not compatible with the connected device. # (device.profile_synchronised() is False.) # # After each build, we cache the build info and use it for future # reference. If build info is not stored for this app and base version, # we build the app anyway and cache the build info. # Push the app and fetch the updated settings if successful push(track_event=False) updated_app_data = siphon.get_app(conf.app_id) version = updated_app_data['base_version'] # Create a Build instance for the app build = Build(conf.app_id, version, dev_mode) # Make sure the correct base version is installed and the build # directory exists ensure_base(version) build.ensure_build_dir() # The new display name defaults to the app name if it is not set display_name = updated_app_data.get('display_name') if not display_name: display_name = updated_app_data.get('name') facebook_app_id = updated_app_data.get('facebook_app_id') bundle_id = 'com.getsiphon.%s' % conf.app_id # Initialize a BaseConfig instance that will be used to configure # the base project we're archiving. base_conf = BaseConfig(display_name, bundle_id, facebook_app_id=facebook_app_id) # Get the cached build info for the previous build cache = Cache() build_info_updated = cache.build_info_updated(build.build_id, display_name, facebook_app_id) # If the build info has been updated, we clear the old archives if build_info_updated: build.clean_archives() # If the provisioning profile needs updating, then we clear the archive # of the device build only. (The profile doesn't affect # other builds for the simulator). # Is the installed profile compatible with the connected device? profile_synchronised = device.profile_synchronised() if not profile_synchronised: build.clean_archive(device.formatted_name) # Sort out a new provisioning profile that includes the device device.update_requirements() # Was previous build performed with the installed profile? If we're # here then a compatible profile is installed. build_profile_ok = device.build_profile_ok(build.build_id) if not build_profile_ok: build.clean_archive(device.formatted_name) # If an archive doesn't exist at this point (perhaps it never existed, # or was deleted when invalidated above) we need to rebuild. if not build.archived(device.formatted_name): archive = True else: archive = False arch_dir = build.archive_dir_path(device.formatted_name) # Archiving takes a while, so prompt the user if archive: proceed = yn('A build is required for this app. ' \ 'This step is needed if you haven\'t run this ' \ 'app on the device before, ' \ 'you have changed the name or base version, or ' \ 'if an update has recently been performed. ' \ 'This may take a few minutes. Proceed? ' \ '[Y/n]: ') if proceed: try: provisioning_profile = cache.get_provisioning_profile_id() base = BaseProject(build.project_dir(platform)) with base.configure(base_conf): app = device.archive(base.directory, arch_dir, conf.app_id, auth.auth_token, provisioning_profile, dev_mode) # Update the cached build info cache.set_build_info(build.build_id, display_name, facebook_app_id) # Record the provisioning profile used to archive this app cache.set_build_profile(build.build_id, provisioning_profile) except SiphonDeviceException: cleanup_dir(arch_dir) sys.exit(1) except KeyboardInterrupt: puts(colored.red('\nArchiving interrupted')) cleanup_dir(arch_dir) sys.exit(1) else: sys.exit(1) else: app = device.app_path(arch_dir) device.run(app)