Question writing script to make image

Hi all,

I have this I am in the process of writing to create custom images for nano:

#!/usr/bin/env python3

# Copyright 2019 Michael de Gans
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.

import os
import sqlite3
import subprocess

def choose_jetpack(jetpacks):
    print('Multiple JetPacks found. Please choose from the following:')
    for i, jetpack in enumerate(jetpacks):
        print(f'{i}: {jetpack[0]} for hardware: {jetpack[1]} at {jetpack[2]}')
    choice = None
    while choice not in range(len(jetpacks)):
        try:
            choice = int(input(f'Choice (0 to {len(jetpacks) - 1})?'))
        except ValueError:
            pass
    return jetpacks[choice]

def get_jetpack_home():

    dbfile = os.path.join(os.path.expanduser('~'), '.nvsdkm', 'sdkm.db')

    if not os.path.isfile(dbfile):
        raise RuntimeError(f'Sdk manager does not appear installed. '
                           f'Cannot find db at {dbfile}')

    # TODO: add good, persistent, install url to error

    with sqlite3.connect(dbfile) as conn:  # type: sqlite3.Connection
        cur = conn.cursor()
        cur.execute(
            "SELECT title, targetOS, targetHW, targetImageDir FROM bundles")
        jetpacks = []
        for title, target_os, target_hw, target_image_dir in cur:
            if target_hw != 'host' and target_os == 'Linux':
                jetpacks.append((title, target_hw, target_image_dir))
        if not jetpacks:
            raise RuntimeError(
                'No installed bundles found. Please run SDKManager to install '
                'the bundles for your hardware.')
        if len(jetpacks) > 1:
            jetpack = choose_jetpack(jetpacks)
        else:
            jetpack = jetpacks[0]

    return jetpack[2]  # (target_image_dir)

def sudo_debootstrap(rootfs,
                     version='bionic',
                     include=None,
                     components=('restricted', 'universe', 'multiverse'),
                     ):
    print('Running debootstrap on rootfs...')

    cmd = ['sudo', 'debootstrap', '--arch=arm64', '--foreign', '--verbose',
           '--keyring=/usr/share/keyrings/ubuntu-archive-keyring.gpg']

    # if any packages to include or components to add, add the options:
    if include:
        if len(include) == 1 and os.path.isfile(include):
            with open(include[0]) as f:
                include = {line.strip() for line in f}
        cmd.append(f'--include={",".join(include)}')
    if components:
        cmd.append(f'--components={",".join(components)}')

    subprocess.run((*cmd, version, rootfs)).check_returncode()

def main(outfile, image_size='8G', **kwargs):

    # some important paths
    jetpack_home = get_jetpack_home()
    l4t_dir = os.path.join(jetpack_home, 'Linux_for_Tegra')
    rootfs_dir = os.path.join(l4t_dir, 'rootfs')

    # following this documentation more or less:
# https://docs.nvidia.com/jetson/l4t/Tegra%20Linux%20Driver%20Package%20Development%20Guide/rootfs_custom.html

    # make sure rootfs dir exists, if it doesn't, something is very wrong
    if not os.path.isdir(rootfs_dir):
        raise RuntimeError(f'rootfs directory not found at {rootfs_dir}')

    # clean out rootfs folder:
    delete_cmd = f'sudo rm -rf {rootfs_dir}/*'
    while os.listdir(rootfs_dir):
        response = input(f'{rootfs_dir} directory is not empty. Would you like '
                         f'to execute "{delete_cmd}" y/n ?').lower()
        if response == 'y':
            subprocess.run(delete_cmd, shell=True).check_returncode()
        elif response == 'n':
            print(
                f'{rootfs_dir} must be empty to continue. The delete command can'
                f' be run manually with "{delete_cmd}"')

    sudo_debootstrap(rootfs_dir, **kwargs)

    print('Applying binaries...')
    subprocess.run(
        os.path.join(l4t_dir, 'apply_binaries.sh')).check_returncode()

    # DEBS install here...

    # building the image
    print('Building flashable image...')
    img_script = os.path.join(l4t_dir, 'create-jetson-nano-sd-card-image.sh')
    subprocess.run(
        (img_script, '-o', outfile, '-s', image_size, '-r', '200')
    ).check_returncode()

    print('Done... Image ready for sd card flash.')

def cli_main():

    import argparse

    component_choices = {'restricted', 'universe', 'multiverse'}
    # TODO: test with xenial
    valid_ubuntu_versions = {'bionic'}

    ap = argparse.ArgumentParser(
        description='Builds a custom Ubuntu image for NVIDIA Jetson Nano')

    ap.add_argument('outfile', help='image filename to write out to.')

    ap.add_argument('--version', default='bionic', choices=valid_ubuntu_versions,
                    help='version of Ubuntu to bootstrap')

    ap.add_argument('--apt-packages', nargs='+', dest='include',
                    help='list of apt packages / meta-packages to be installed'
                         '(text file with list of apt packages also accepted)')

    ap.add_argument('--components', nargs='+', choices=component_choices,
                    default=component_choices,
                    help='this sets which apt sources are used (default, all).'
                         'see debootstrap --components flag)')

    # todo: automatically calculate this:
    ap.add_argument('--image-size', default='8G',
                    help='-s option for create-jetson-nano-sd-card-image.sh')

    main(**vars(ap.parse_args()))

if __name__ == '__main__':
    cli_main()

… and it would be great to have some help help where it says “# DEBS install here…”

SDKM downloads a bunch of .deb files to a download path, but so far as I can find, that path is not stored in sdkm.db with all the other sdkm details.

I can parse it from the log, but that’s ugly and the files could have changed. I could guess at it but same problem.

The only other way I can think of to clean .deb files is to use the stored token in the db along with something like requests_oauth and get the debs from the urls in the json, but that’s a lot of work (although I did start, see below for snippet).

...

MAIN_REPO_URL = 'https://developer.download.nvidia.com/sdkmanager/sdkm-config/main/sdkml1_repo.json'

def get_jetson_linux_repo_json(session):
    main_repo_config = session.get(MAIN_REPO_URL).json()

    product_categories = main_repo_config['productCategories']
    for category in product_categories:
        if category['categoryName'] == 'Jetson':
            jetson_category = category
            break
    else:
        raise RuntimeError('Jetson category not found product categories.')

    product_lines = jetson_category['productLines']
    for product_line in product_lines:
        if product_line['targetOS'] == 'Linux':
            release_url = product_line['releasesIndexURL']
            break
    else:
        raise RuntimeError('Linux not found in product lines for Jetson.')

    # figure out the absolute path to the jetson json and return it

    main_url = urlparse(MAIN_REPO_URL)   # 6 tuple of Text
    scheme, netloc, path, params, query, fragment, = main_url

    dirname = os.path.dirname(os.path.dirname(path))
    # there may be a better way to handle the ../ but it works:
    json_path = os.path.join(dirname, release_url.lstrip('../'))

    jetson_json_url = urlunparse((  # same 6 tuple of text with modified path
        scheme, netloc, json_path, params, query, fragment))

    return session.get(jetson_json_url).json()

def get_jetpack_json(session):
    """gets jetpack json from an authorzed session"""
    repo_json = get_jetson_linux_repo_json(session)
    latest_release = repo_json['releases'][-1]
    return session.get(latest_release['compRepoURL']).json()

...

Is there a tarball somewhere with .deb files exclusively for arm64 containing things like cudnn and libvisionworks, or better yet, can apply_binaries.sh install this stuff as an option?

Sorry, for cudnn and visionworks need install after boot to tegra currently.

Thanks for your response.

I was planning on using qemu to install the apt packages on the rootfs. Will that not work for sure, or is it just untested?

If it doesn’t and the installation requires actual hardware, my backup idea is a first boot script and a systemd unit file (like you guys did with your nvfb) to install the .deb files and make any other changes I would have made with qemu.

Either way, however, I need those .deb files and I’d rather have a proper than a hacky solution (parsing log, checking against a list of shas) or one that takes a long time to write (and debug).

Can a public tarball with the .deb files on this page or an online apt repository be arranged? I understand if you need people to agree to a license agreement, but I believe there is a way you can require that when the apt repository is added.

Hi,

We don’t have an experiment on qemu for installing the packages.
You can just give it a try.

After executing sdkmanager, all the package can be found in ${HOME}//Downloads/nvidia/sdkm_downloads/.
Maybe you can include all the required packages into your customized image.

Does this meet your requirement?

Thanks.

Thanks. I have done it before with Pi, so I will give it a try on Nano if I can resolve the packaging situation.

Unfortunately, no. What I am looking for is:

  • get your gpg key
  • verify some shas
  • download some .deb files
  • verify said deb files

… and then install them with qemu on the rootfs.

Your proposed solution requires that one use SDK manager and somehow trust that the right files are there in a folder that is not guaranteed to exists, and haven’t been modified since, either accidentally or intentionally. I want this script to ‘just work’ on anybody’s system and not rely on anything I can’t control (especially a Downloads folder that many people regularly clean out or add things to).

Having these packages available easily separately, is really all I want on Nano right now, as it would solve a bunch of separate issues. Can the downloads please, please, please not require SDK Manager. Can a tarball with these downloads please be arranged?

Edit:

I don’t mean to sound like I’m complaining. I like the Nano and Nvidia, I do. It just seems like I’m being forced into using some GUI application and that’s getting in the way of my ability to automate things the way I want the way I’m used to.

Ideally, I want to be able to install SDKM in a Ubuntu 18 based docker image and have it’s container plop out a flashable image with tweaked kernel, rootfs, etc (controlled by an entrypoint script), and right now that’s frustratingly difficult to do the ‘right way’ and hacking around it triggers my OCD.

Could SDKM have a cli install maybe? That would solve my issue in a roundabout manner.

Hi,

It’s okay. Let me check this and update with you later.
Thanks.

Thanks for understanding. Sorry if I came off like kind of a dick. I’m trying to pull a Linus and work on that (with hopefully better results).

Hi,

We have filed an internal bug for you.
Will share information with you once we got a response.

Thanks.

Hi,

Sorry for keeping you waiting. Here are the reply from our internal team.

SDKmanager does support downloading with cli mode, for example, following command will download all components for Xavier board.

sdkmanager --cli downloadonly --user <fill_in_actual_user_email_address> --logintype devzone --product Jetson --version 4.2 --targetos Linux --host --target P2888 --license accept

Please get more information on CLI mode by running “sdkmanager --help” command.
Note that although downloading with CLI mode works, there are bugs in installation with CLI mode.
We are trying to add more CLI mode support for JetPack in the coming release.

Thanks.

I should have tried a --help. This solves a lot of problems. Thank you so much, and if I run into any bugs I can replicate I will report them.