FYI: Older 32-bit iOS apps will not install on iOS 11.

dfrancisco
New Contributor

Hi. I have been slamming my head against the wall for the past few weeks trying to figure out why certain apps would not go through our VPP account on our new iOS 11 iPads. I finally found the answer yesterday.

We purchased the apps through our VPP account. We see the apps in our JSS. We can see the apps are correctly scoped to our new iPads. But for some reason, some apps will not install while others can.

I learned that two kinds of iOS apps exist:

  • 32-bit apps
  • 64-bit apps

iOS 10 and below can support both 32-bit apps and 64-bit apps. However, iOS 11 will only support 64-bit apps, and not 32-bit apps.

But how do you know if the app is 32-bit?

Find an app that you're having trouble installing on your iOS 11 iPad. Here is our self service. Any app with a checkmark was able to go through and was installed properly. Any app that has the [INSTALL] button next to it means the app was scoped out correctly to the iPad, but it has not been installed.

83659215977044789523253e84abf777

Let's check out the "Amazing Space Journey" app on the left column. The app has the [INSTALL] button, meaning it was scoped to the iPad correctly, but did not install for some reason.

Inside the "Amazing Space Journey" app at the very bottom is the "App URL".

31c597d5a4874041b4be9508db2c1a14

[https://itunes.apple.com/us/app/amazing-space-journey-3d-solar-system/id579895132?mt=8&ign-mpt=uo%3D4](link URL)

This is the app store link for "Amazing Space Journey". Let's copy this link, and open it up in a browser to check the app out.

On the app store preview page for "Amazing Space Journey", there is a "Version History" section that you can look at. Let's take a look at it.

667e866108e148b7a0470a01f87dd453

The last time this app was updated was on December 14, 2012. A loooong time ago. iOS 11 was released on September 19, 2017. This most likely means that the developer of the app has never updated their app from 32-bit up to 64-bit.

This probably explains why we can't install "Amazing Space Journey" on our new iOS 11 iPads. The app is probably still 32-bit, and 32-bit apps can't work on iOS 11.

SUMMARY
- There are 32-bit apps, and 64-bit apps
- Only 64-bit apps work on iOS 11
- 32-bit apps will appear in self service, but not install on iOS 11
- Apps that haven't been updated in years are probably still 32-bit
- Apps that have been updated recently are most likely 64-bit

I hope I wasn't alone in this, and I hope this helps some other poor soul!

5 REPLIES 5

CasperSally
Valued Contributor II

@dfrancisco

I've asked apple to clearly identify apps that are 32bit only in the VPP store so that our users know to NOT buy them. If you have apple enterprise support, I highly suggest you putting in a ticket asking for same thing. It would be great if you referenced my ticket 100331111842 to piggy back on to the cause :)

For now, I worked with jamf and they gave me a script to run pointing out which of our existing apps are 32bit only, and I put a note in the spreadsheets we share with the app purchasers to not buy anymore copies of those apps.

I bullied apple for weeks and requested VPP credits for recent 32bit app purchases because there is no note they won't work with their latest operating system. It's been months and i've been promised the credits, but I don't believe we've actually received the credits. I just emailed them again asking for the credit.

JSS 10.1.1 does a good job of pointing out apps with an "alert" symbol that are no longer in the US store and no longer available for us to push out. I'd love to see this extended for 32bit apps in some way. This feature request I entered may be of some interest to you to allow us to sort by these alerts in order to more easily clean them up.

https://www.jamf.com/jamf-nation/feature-requests/6868/allow-us-to-sort-or-search-for-apps-removed-f...

cdenesha
Valued Contributor II

Good stuff.

A definitive way is to view that app in the App Store from an iOS 11 iPad. If you can't find it, then it is no longer 'for sale' and probably 32 bit.

If you can find it and it is 32 bit, at the top it will say "The developer of this app needs to update it to work with iOS 11".

8f59ace4dbf443d98220956d48c3b31d

chris

joshuasee
Contributor III

You can check the minimum iOS version supported to have a better feel for 64-bit support. iOS 7 made it mandatory.

Any app submitted to the app store after February 1, 2015 includes 64-bit support, as does any app updated after June 1, 2015.

nsdjoe
Contributor II

If you are on iOS 10.3.x and want to see what apps on your iPad are 32-bit, go to Settings -> General -> About. Click on Applications. You will see a list of apps that are still 32-bit. These apps will not work with iOS 11 unless they are updated. See if there is an update available in the App Store (or Self Service). If there is no update, then these apps will not work with iOS 11.

If you click Settings -> General -> About -> Applications, and no list of apps shows up (i.e. it is greyed out and can’t be clicked), it means you don't have any apps installed that are 32-bit and all of your currently installed apps should work after the iOS 11 update.

~Joe

dnabedrik
New Contributor II

I've had issues where some of these apps caused major errors in the JSS logs. After working with Jamf Support they gave me a python script, all you have to do is save your full JSS summary as a .txt file and then run the python file. It will then spit out a .csv file with all the apps that you have in your list that aren't on the App Store anymore.

#!/usr/bin/env python
####################################################################################################
#
# Copyright (c) 2014, JAMF Software, LLC.  All rights reserved.
#
#       Redistribution and use in source and binary forms, with or without
#       modification, are permitted provided that the following conditions are met:
#               * Redistributions of source code must retain the above copyright
#                 notice, this list of conditions and the following disclaimer.
#               * Redistributions in binary form must reproduce the above copyright
#                 notice, this list of conditions and the following disclaimer in the
#                 documentation and/or other materials provided with the distribution.
#               * Neither the name of the JAMF Software, LLC nor the
#                 names of its contributors may be used to ENDorse or promote products
#                 derived from this software without specific prior written permission.
#
#       THIS SOFTWARE IS PROVIDED BY JAMF SOFTWARE, LLC "AS IS" AND ANY
#       EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
#       WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
#       DISCLAIMED. IN NO EVENT SHALL JAMF SOFTWARE, LLC BE LIABLE FOR ANY
#       DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
#       (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
#       LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
#       ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
#       (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
#       SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#
####################################################################################################
#
#   Date: 04/14/2017
#   Created by: Jordan Wisniewski
#
#   This script will parse a JSS Summary, or search the API for Mac and Mobile Device apps,
#   and it will check to determine if they still exist on the App Store. If they do not exist
#   a CSV will be made with the app information, and if using the API, VPP will get disabled.
#
#   NOTE: The 'requests' library is requiRED for this script. Uncomment the lines below to force
#         the installation of 'requests' and/or 'pip'
#
#   MORE INFO: https://github.com/kennethreitz/requests, https://pypi.python.org/pypi/pip
#
####################################################################################################

from xml.etree import ElementTree as et
from contextlib import contextmanager
from time import sleep
#import requests
import site
import sys, os
import getpass
import time
import json
import re

@contextmanager
def suppress_stdout():
    with open(os.devnull, "w") as devnull:
        old_stderr = sys.stderr
        old_stdout = sys.stdout
        sys.stdout = devnull
        sys.stderr = devnull
        try:  
            yield
        finally:
            sys.stdout = old_stdout
            sys.stdout = old_stderr

try: 
    with suppress_stdout():
        from setuptools.command import easy_install
        try:
            import pip
        except Exception as e:
            easy_install.main(["-U", "pip"])
            reload(site)
            import pip
        try:
            pip.main(['install', 'requests'])
            pip.main(['install', 'requests[security]'])
            reload(site)
            #pip.main(['install', '--upgrade', 'requests[security]', '--ignore-installed', 'six'])
        except Exception as e:
            print "Error: " + str(e)
            sys.exit(1)
    import requests
except Exception as e:
    print "Error: " + str(e)
    sys.exit(1)

from requests.packages.urllib3.exceptions import InsecureRequestWarning
requests.packages.urllib3.disable_warnings(InsecureRequestWarning)

class App:
    def __init__(self):
        self.type = ''
        self.id = 0
        self.name = ''
        self.adam_id = 0
        self.url = ''
        self.country = ''
        self.vpp = 'Unknown'

class Color:
    GREEN = '�33[92m�33[1m'
    RED = '�33[91m�33[1m'
    BLUE = '�33[94m�33[1m'
    END = '�33[0m'

#clear the command line window      
os.system('cls' if os.name == 'nt' else 'clear')

#### GLOBAL ###
ITUNES_URL = 'https://itunes.apple.com/lookup'
MOBILE_APP_URI = '/JSSResource/mobiledeviceapplications'
MAC_APP_URI = '/JSSResource/macapplications'
JSON_HEADER = {'Accept': 'application/json', 'Content-type':'application/json'}
XML_HEADER = {'Accept': 'text/xml', 'Content-type':'text/xml'}
VERIFY_SSL = False

### USER INPUT ###
JSS_URL = None
JSS_USER = None
JSS_PASS = None
FILE_PATH = None

def main():
    global JSS_URL
    global JSS_USER
    global JSS_PASS
    global FILE_PATH

    valid = False
    while not valid:
        input_source = raw_input('enter 1 to parse jss summary, or 2 to query api: ').strip()

        if input_source == '1':
            valid = True
            FILE_PATH = raw_input('Path to JSS Summary: ').strip()
            findAppsNotInAppStoreViaSummary()
        elif input_source == '2':
            valid = True
            JSS_URL = raw_input('JSS URL: ').strip()
            JSS_USER = raw_input('JSS User: ').strip()
            JSS_PASS = getpass.getpass('JSS Pass: ').strip()
            fixAppsNotInAppStoreViaApi()

def findAppsNotInAppStoreViaSummary():
    try:
        apps_not_in_appstore = []
        with open(FILE_PATH, 'rb') as f:
            content = f.readlines()
            for index, line in enumerate(content):
                if line.strip().startswith('iTunes ID'):
                    app_block = content[max(0, index - 6):index + 1]
                    app = App()
                    for attribute in app_block:
                        attribute = attribute.strip()
                        if attribute.startswith('ID'):
                            app.id = attribute.split()[1]
                        if attribute.startswith('Name'):
                            app.name = re.split('.+', attribute)[1].strip()
                        if attribute.startswith('iTunes Store URL'):
                            app.url = re.split('.+', attribute, maxsplit=1)[1].strip()
                            app.country = app.url.split('/')[3]
                        if attribute.startswith('iTunes ID'):
                            app.adam_id = re.split(r's{2,}', attribute)[1]
                    if appInAppStore(app) is False:
                        apps_not_in_appstore.append(app)
                progressBar('lines scanned', index, len(content))
        print " - done!"
        output('apps', apps_not_in_appstore)
    except Exception as e:
        print 'Error finding apps in the JSS Summary that are no longer on the App Store: ' + str(e)

def fixAppsNotInAppStoreViaApi():
    try:
        checkJssAppsInAppStore('mac_apps', getListOfApps('mac'))
        checkJssAppsInAppStore('mobile_apps', getListOfApps('mobile'))
    except Exception as e:
        print 'Error processing apps: ' + str(e)

def checkJssAppsInAppStore(type, app_list):
    apps_not_in_appstore = []
    try:
        for index, app in enumerate(app_list):
            progressBar('checking ' + type + ': ', index, len(app_list))
            getAppDetails(app)
            if app.url is not None:
                if appInAppStore(app) is False:
                    if app.vpp is True:
                        r = disableVpp(app)
                        if r == requests.codes.created:
                            app.vpp = False             
                    apps_not_in_appstore.append(app)    
        print " - done!"
        output(type, apps_not_in_appstore)
    except Exception as e:
        print 'Error finding and fixing apps in the JSS that are no longer on the App Store: ' + str(e)

def progressBar(title, index, total):
    chunks = 50
    current = int(chunks * (index + 1) / total)
    sys.stdout.write('
{0} [{1}{2}] {3}/{4}'.format(
                            title + ':', 
                            Color.BLUE + ('*' * current) + Color.END, 
                            ' ' * (chunks - current), 
                            (index + 1), 
                            total
                     )
    )
    sys.stdout.flush()

def output(type, app_list):
    try:
        if app_list is not None and len(app_list) > 0:
            output_path = '/tmp/' + str(int(time.time() * 1000)) + '_missing_' + type + '.csv'
            with open(output_path, 'w') as out:
                out.write('"{0}", "{1}", "{2}", "{3}", "{4}", "{5}",
'.format(
                                "App ID", 
                                "App Name", 
                                "Adam ID", 
                                "Country", 
                                "URL", 
                                "VPP Enabled"
                            )
                )
                for app in app_list:
                    out.write('"{0}", "{1}", "{2}", "{3}", "{4}", "{5}",
'.format(
                                app.id,
                                app.name,
                                app.adam_id,
                                app.country,
                                app.url, 
                                app.vpp
                            )
                    )
                    if app.vpp is True:
                        print (Color.RED + "failed to disable vpp" + Color.END + " for id: {0} | adam id: {1} | name: {2}").format(
                            Color.BLUE + app.id + Color.END,
                            Color.BLUE + app.adam_id + Color.END,
                            Color.BLUE + app.name + Color.END
                        )
            print Color.RED + type + ' were found missing from app store. ' + Color.GREEN + 'an attempt was made to disable vpp if it was enabled, and if using api query method. '
            print Color.BLUE + 'details found in: ' + output_path + '
' + Color.END
        else:
            print Color.GREEN + 'all ' + type + ' appear to be on the App Store.' + u'U0001F44D' + '
' + Color.END
    except Exception as e:
        print 'Error attempting to generate output: ' + str(e)

def disableVpp(app):
    try:
        use_vpp = "false"
        vpp_account = "-1"
        if app.type == 'mac':
            app_el = et.Element('mac_application')
        elif app.type == 'mobile':
            app_el = et.Element('mobile_device_application')
        vpp_el = et.SubElement(app_el, 'vpp')
        dvpp_el = et.SubElement(vpp_el, 'assign_vpp_device_based_licenses')
        dvpp_el.text = use_vpp
        acc_el = et.SubElement(vpp_el, 'vpp_admin_account')
        acc_el.text = vpp_account
        xml = et.tostring(app_el, encoding='UTF-8')
        r = requests.put(JSS_URL + MOBILE_APP_URI + '/id/' + app.id, headers=XML_HEADER, auth=(JSS_USER, JSS_PASS), data=xml, verify=VERIFY_SSL)
        return r.status_code
    except Exception as e:
        print 'Error disabling VPP for app not found in App Store: ' + str(e)

def getAdamId(url):
    try:
        url_END = url.rsplit('/', 1)[-1]
        start = url_END.index('id') + len('id')
        END = url_END.index('?', start)
    except Exception as e:
        pass
    return str(url_END[start:END]).encode('utf8')

def appInAppStore(app):
    try:
        r = getJsonAppFromAppStore(app.country, app.adam_id)
        results = r['resultCount']
        if results > 0:
            return True
        else:
            return False
    except Exception as e:
        print 'Error checking App Store for app: ' + str(e)

def getJsonAppFromAppStore(country, adam_id):
    params = {'id':adam_id, 'country':country}
    try:
        r = requests.get(ITUNES_URL, headers=JSON_HEADER, params=params, verify=VERIFY_SSL)
    except Exception as e:
        print 'Error checking App Store for app: ' + str(e)   
    return r.json()

def getJsonAppFromJSS(uri, app_id):
    try:
        r = requests.get(JSS_URL + uri + "/id/" + str(app_id), auth=(JSS_USER, JSS_PASS), headers=JSON_HEADER, verify=VERIFY_SSL).json()
    except Exception as e:
        print 'Error getting app details from JSS: ' + str(e)
    return r

def getAppDetails(app):
    try:
        if app.type == 'mac':
            json = getJsonAppFromJSS(MAC_APP_URI, app.id)
            app.url = json['mac_application']['general']['url'].encode('utf8')
            app.country = app.url.split('/')[3]
            app.adam_id = getAdamId(app.url)
            app.vpp = json['mac_application']['vpp']['assign_vpp_device_based_licenses']
        elif app.type == 'mobile':
            json = getJsonAppFromJSS(MOBILE_APP_URI, app.id)
            app.url = json['mobile_device_application']['general']['itunes_store_url'].encode('utf8')
            app.country = json['mobile_device_application']['general']['itunes_country_region'].encode('utf8')
            app.vpp = json['mobile_device_application']['vpp']['assign_vpp_device_based_licenses']
            app.adam_id = getAdamId(app.url)
    except Exception as e:
        pass

def getJsonAppListFromJSS(uri):
    try:
        r = requests.get(JSS_URL + uri, auth=(JSS_USER, JSS_PASS), headers=JSON_HEADER, verify=VERIFY_SSL).json()
    except Exception as e:
        print 'Error getting app list from JSS: ' + str(e)
    return r

def getListOfApps(type):
    app_list = []
    try:
        if type == 'mac':
            json = getJsonAppListFromJSS(MAC_APP_URI)
            dict = 'mac_applications'
        elif type == 'mobile':
            json = getJsonAppListFromJSS(MOBILE_APP_URI)
            dict = 'mobile_device_applications'
        for json_app in json[dict]:
            app = App()
            app.id = str(json_app['id']).encode('utf8')
            app.name = json_app['name'].encode('utf8')
            app.type = type
            app_list.append(app)
    except Exception as e:
        print 'Error parsing app list from JSS: ' + str(e)
    return app_list

main()