def create(app_name, app_path): auth = Auth() # Create the app server-side siphon = Siphon(auth.auth_token) obj = siphon.create(app_name) app_id = obj['id'] # server gives us back our internal app ID # Populate our new directory with template files copy_app_template(app_name, app_path) # Write out a .siphon configuration to the new direcotry conf = Config(directory=app_name) conf.app_id = app_id # Copy our .siphonignore file over siphon_ignore = os.path.join(CLI_RESOURCES, '.siphonignore') shutil.copyfile(siphon_ignore, os.path.join(app_path, '.siphonignore')) puts(colored.green('Siphon app created at %s' % app_path)) # Register Mixpanel event username = auth.username mixpanel_event(MIXPANEL_EVENT_CREATE, username, {'app_id': app_id, 'existing_app': False}) # Write out the Siphonfile with open(os.path.join(app_name, SIPHON_USER_CONFIG), 'w') as fp: json.dump({'base_version': obj['base_version']}, fp, indent=2) # Implicitly do a push too with cd(app_path): from siphon.cli.commands.push import push push(track_event=False) puts(colored.green('Done.'))
def run(args): # Generate/retrieve the user's auth token credentials = request_login() username = credentials['username'] auth_token = credentials['auth_token'] # Update our .auth file auth = Auth() auth.username = username auth.auth_token = auth_token mixpanel_event(MIXPANEL_EVENT_LOGIN)
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 develop(sim, default_sim=True, global_watchman=False): """ Takes a simulator object and runs the simulator """ conf = Config() user_conf = UserConfig() app_id = conf.app_id mixpanel_props = { 'app_id': app_id, 'simulator': sim.formatted_name, 'default_sim': default_sim } mixpanel_event(MIXPANEL_EVENT_DEVELOP, 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 # # 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. # Here we use the version in the Siphonfile since we are not pushing # the app. version = user_conf.get('base_version') # Ensure that node exists in our develop directory ensure_node(version) # Create a Build instance for the app build = Build(conf.app_id, version) # Make sure the correct base version is installed and the build # directory exists ensure_base(version) build.ensure_build_dir() # Get/set the display name display_name = user_conf.get('display_name') if not display_name: print('A display name for this app has not been set. ' \ 'Please enter a display name. This can be changed by ' \ 'modifying the "display_name" value in the app ' \ 'Siphonfile.') new_display_name = get_input('Display name: ') user_conf.set('display_name', new_display_name) display_name = new_display_name # Get the facebook_app_id facebook_app_id = user_conf.get('facebook_app_id') 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() # Make sure our develop directory is populated with the latest node modules # and is generally Initialized dev_dir = DevelopDir(version) dev_dir.clean_old() dev_dir.ensure_develop_dir() 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, app_transport_exception='localhost') base = BaseProject(build.project_dir(sim.platform)) platform_version = sim.platform_version arch_dir = build.archive_dir_path(sim.formatted_name, platform_version) if not build.archived(sim.formatted_name, platform_version): with base.configure(base_conf): app = archive(sim, arch_dir, build.project_dir()) else: app = sim.app_path(arch_dir) # Update the cached app_info & run the app cache.set_build_info(build.build_id, display_name, facebook_app_id) start_processes(dev_dir, sim, app, bundle_id, global_watchman)
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)
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 init(): auth = Auth() # Create the app server-side siphon = Siphon(auth.auth_token) print('Please enter a name for your Siphon app.') try: name_valid = False while not name_valid: app_name = get_input('App name: ') name_valid = app_name_valid(app_name) if not name_valid: print('Siphon app names may only contain letters, numbers, ' \ 'underscores and hyphens.') except KeyboardInterrupt: sys.exit(1) obj = siphon.create(app_name) app_id = obj['id'] # server gives us back our internal app ID # Write out a .siphon configuration to the new directory conf = Config() conf.app_id = app_id # Copy our .siphonignore file over siphon_ignore = os.path.join(CLI_RESOURCES, '.siphonignore') shutil.copyfile(siphon_ignore, '.siphonignore') puts(colored.green('Siphon app created.')) # Register Mixpanel event username = auth.username mixpanel_event(MIXPANEL_EVENT_CREATE, username, {'app_id': app_id, 'existing_app': True}) # Write out the Siphonfile with open(SIPHON_USER_CONFIG, 'w') as fp: json.dump({'base_version': obj['base_version']}, fp, indent=2) # Implicitly do a push too from siphon.cli.commands.push import push push(track_event=False) puts(colored.green('Done.')) # Print warning about mismatched React Native versions project_rn_version = node_module_version('node_modules/react-native') siphon_rn_version = obj['react_native_version'] if project_rn_version != siphon_rn_version: puts(colored.yellow('Note: Siphon app is using React Native %s but ' \ 'existing project is using React Native %s.\n'\ 'Please visit https://getsiphon.com/docs/base-version/ to learn ' \ 'more about base versions.\n' % (siphon_rn_version, project_rn_version))) if os.path.isfile('index.js'): print('You must register your component with the name \'App\'' \ 'in your index.js file to use Siphon.\n') if os.path.isfile('index.js'): print_app_registry_message('index.js') else: print('You must register your component with the name \'App\'' \ 'in your index.ios.js and index.android.js files ' \ ' to use Siphon.\n') if os.path.isfile('index.ios.js'): print_app_registry_message('index.ios.js') if os.path.isfile('index.android.js'): print_app_registry_message('index.android.js')
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)
def post_archive(bundler_url, server_hashes): puts(colored.yellow('Hashing...')) # Generate a .zip archive for our payload, writing out a listing file, and # any new/changed files into the diffs/ directory. fp = BytesIO() ignore_patterns = get_ignored() with zipfile.ZipFile(fp, 'w') as zf: listing = {} for root, dirs, files in os.walk('.'): if root == '.': prefix = '' elif root.startswith('./'): prefix = root[2:] + '/' else: raise RuntimeError('Unexpected root "%s"' % root) dirs[:] = keep(dirs, ignore_patterns, prefix) for fil in keep(files, ignore_patterns, prefix): local_path = root + '/' + fil # relative to current directory remote_path = prefix + fil # relative to app root zip_path = 'diffs/' + remote_path # path within .zip archive # Generate SHA-256 for this file and record it in the listing sha = sha256(local_path) listing[remote_path] = sha # Only write this file into diffs/ if we need to if should_push(sha, remote_path, server_hashes): zf.write(local_path, zip_path) # Write out the listing file JSON zf.writestr('listing.json', json.dumps(listing)) # Wrap it in a ProgressReader. It logs a progress bar to console as # python-requests cycles through the bytes doing the POST below. fp.seek(0) reader = ProgressReader(fp.read()) fp.close() # Do the push. puts(colored.yellow('Pushing to the bundler...')) response = requests.post(bundler_url, data=reader, stream=True, headers={ 'Accept-Encoding': 'gzip;q=0,deflate,sdch' # explicitly disables gzip }, timeout=(10, None)) # unlimited read timeout reader.close() if response.ok: msg = None for line in response.iter_lines(): l = line.decode('utf-8') if 'ERROR' in l: msg = l if len(msg) > 200: msg = msg[0:200] print(l) if msg: mixpanel_props = {'error': msg} mixpanel_event(MIXPANEL_EVENT_PUSH_ERROR, properties=mixpanel_props) return False else: msg = response.content.decode('utf-8') if len(msg) > 200: msg = msg[0:200] mixpanel_props = {'error': msg} mixpanel_event(MIXPANEL_EVENT_PUSH_ERROR, properties=mixpanel_props) raise SiphonBundlerException('Problem writing changes: %s' % response.content) return True