├── _config.yml
├── log
└── .gitignore
├── .gitignore
├── default-bashrcs
├── README.md
└── .bashrc-community
├── aliases.sh
├── .editorconfig
├── powerline.sh
├── .config
└── powerline-shell
│ └── config.json
├── commands
├── update
│ └── __init__.py
├── uninstall
│ └── __init__.py
├── __init__.py
├── panda
│ └── __init__.py
├── README.md
├── debug
│ └── __init__.py
├── device
│ └── __init__.py
├── base.py
└── fork
│ └── __init__.py
├── uninstall.sh
├── CONTRIBUTING.md
├── py_utils
├── colors.py
└── emu_utils.py
├── emu.sh
├── emu.py
├── update.sh
├── check-for-updates.sh
├── README.md
├── CHANGELOG.md
├── install.sh
└── LICENSE
/_config.yml:
--------------------------------------------------------------------------------
1 | theme: jekyll-theme-slate
--------------------------------------------------------------------------------
/log/.gitignore:
--------------------------------------------------------------------------------
1 | **/**
2 | **
3 | !.gitignore
4 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | #IDE Related
2 | .idea/
3 |
4 | # Byte-compiled / optimized / DLL files
5 | __pycache__/
6 | *.py[cod]
7 | *$py.class
8 |
--------------------------------------------------------------------------------
/default-bashrcs/README.md:
--------------------------------------------------------------------------------
1 | A source to /data/community/.bashrc is made in the system .bashrc file, depending on the system (AGNOS or NEOS).
2 |
3 | To edit your own bashrc after installation:
4 | ```
5 | cd /data/community/
6 | ls -al
7 | vi .bashrc
8 | ```
9 |
--------------------------------------------------------------------------------
/aliases.sh:
--------------------------------------------------------------------------------
1 | alias ll="ls -lAh"
2 | alias pf="emu panda flash"
3 | alias controlsd="emu debug controlsd"
4 | alias reload="emu debug reload"
5 | alias battery="emu device battery"
6 | alias shutdown="emu device shutdown"
7 | alias settings="emu device settings"
8 | alias fork="emu fork switch"
9 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | [*]
2 | end_of_line = lf
3 | insert_final_newline = true
4 |
5 | # Matches multiple files with brace expansion notation
6 | # Set default charset
7 | [*.{js,py}]
8 | charset = utf-8
9 |
10 | [*.{py,sh,bashrc-community,bashrc-system}]
11 | indent_style = space
12 | indent_size = 2
13 |
--------------------------------------------------------------------------------
/powerline.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | if [ -x "$(command -v powerline-shell)" ]; then
3 | function _update_ps1() {
4 | PS1=$(powerline-shell $?)
5 | }
6 |
7 | if [[ $TERM != linux && ! $PROMPT_COMMAND =~ _update_ps1 ]]; then
8 | PROMPT_COMMAND="_update_ps1; $PROMPT_COMMAND"
9 | fi
10 | fi
11 |
--------------------------------------------------------------------------------
/.config/powerline-shell/config.json:
--------------------------------------------------------------------------------
1 | {
2 | "segments": [
3 | "virtual_env",
4 | "cwd",
5 | "git",
6 | "git_stash",
7 | "set_term_title",
8 | "root"
9 | ],
10 | "mode": "patched",
11 | "cwd": {
12 | "mode": "flat",
13 | "max_depth": 4
14 | },
15 | "theme": "default"
16 | }
17 |
--------------------------------------------------------------------------------
/commands/update/__init__.py:
--------------------------------------------------------------------------------
1 | from commands.base import CommandBase
2 | from py_utils.emu_utils import run, error
3 | from py_utils.emu_utils import UPDATE_PATH
4 |
5 |
6 | class Update(CommandBase):
7 | def __init__(self):
8 | super().__init__()
9 | self.name = 'update'
10 | self.description = '🎉 Updates this tool'
11 |
12 | @staticmethod
13 | def _update():
14 | if not run(['sh', UPDATE_PATH]):
15 | error('Error updating!')
16 |
--------------------------------------------------------------------------------
/commands/uninstall/__init__.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/python
2 | # -*- coding: utf-8 -*-
3 | from commands.base import CommandBase
4 | from py_utils.emu_utils import run, error, input_with_options, UNINSTALL_PATH
5 |
6 |
7 | class Uninstall(CommandBase):
8 | def __init__(self):
9 | super().__init__()
10 | self.name = 'uninstall'
11 | self.description = '👋 Uninstalls emu'
12 |
13 | @staticmethod
14 | def _uninstall():
15 | print('Are you sure you want to uninstall emu?')
16 | if input_with_options(['Y', 'n'], 'n')[0] == 0:
17 | run(['sh', UNINSTALL_PATH])
18 | else:
19 | error('Not uninstalling!')
20 |
--------------------------------------------------------------------------------
/commands/__init__.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/python
2 | # -*- coding: utf-8 -*-
3 | import os
4 | import importlib
5 | from py_utils.emu_utils import error
6 |
7 | EMU_COMMANDS = []
8 | basedir = os.path.dirname(__file__)
9 | for module_name in os.listdir(basedir):
10 | if module_name.endswith('.py') or module_name == '__pycache__' or not os.path.isdir(os.path.join(basedir, module_name)):
11 | continue
12 | try:
13 | module = importlib.import_module('commands.{}'.format(module_name))
14 | module = getattr(module, module_name.title())
15 | EMU_COMMANDS.append(module())
16 | except Exception as e:
17 | error('Error loading {} command, please try updating!'.format(module_name))
18 | error(e)
19 |
--------------------------------------------------------------------------------
/uninstall.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/bash
2 |
3 | COMMUNITY_BASHRC_PATH=/data/community/.bashrc
4 | OH_MY_COMMA_PATH=/data/community/.oh-my-comma
5 |
6 | echo "Deleting $COMMUNITY_BASHRC_PATH"
7 | rm $COMMUNITY_BASHRC_PATH
8 |
9 | # only applicable on NEOS for now
10 | if [ -f /EON ] && [ -x "$(command -v powerline-shell)" ]; then
11 | echo "It's recommended to uninstall powerline if you uninstall emu. Uninstall powerline?"
12 | read -p "[Y/n] > " choices
13 | case ${choices} in
14 | n|N ) echo "Skipping...";;
15 | * ) mount -o rw,remount /system && pip uninstall powerline-shell && mount -o r,remount /system;;
16 | esac
17 | fi
18 |
19 | echo "Deleting $OH_MY_COMMA_PATH"
20 | rm -rf $OH_MY_COMMA_PATH
21 |
22 | echo "Uninstalled!"
23 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | See First:
2 | https://github.com/AskAlice/.oh-my-comma/projects
3 | https://github.com/AskAlice/.oh-my-comma/issues
4 |
5 | # Contributing
6 | This is a project that will benefit greatly from mass-collaberation, and is somewhat unique in that most users of comma.ai are tech-saavy enough to make a pull request.
7 | Please try to keep your contributions to the core of this repository generic for neos/openpilot, but plugins for .oh-my-comma can be tailored for specific forks if you'd like. (could be cool to see a kegman configuration tool, for example)
8 | Check out the projects and issues on this repo! Feel free to contribute your own ideas as issues, and tag them with labels and projects.
9 |
10 | ## Pull Requests
11 | Ensure that it works locally if possible. I will validate PRs, and make changes to your PRs in the 'devel' branch, if need be.
12 | I will squash and merge all PRs to keep the git log down.
13 |
14 |
15 |
--------------------------------------------------------------------------------
/commands/panda/__init__.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/python
2 | # -*- coding: utf-8 -*-
3 | import importlib
4 | from commands.base import CommandBase, Command
5 | from py_utils.emu_utils import run, error
6 | from py_utils.emu_utils import OPENPILOT_PATH
7 |
8 |
9 | class Panda(CommandBase):
10 | def __init__(self):
11 | super().__init__()
12 | self.name = 'panda'
13 | self.description = '🐼 panda interfacing tools'
14 |
15 | self.commands = {'flash': Command(description='🐼 flashes panda with make recover (usually works with the C2)'),
16 | 'flash2': Command(description='🎍 flashes panda using Panda module (usually works with the EON)')}
17 |
18 | @staticmethod
19 | def _flash():
20 | r = run('make -C {}/panda/board recover'.format(OPENPILOT_PATH))
21 | if not r:
22 | error('Error running make command!')
23 |
24 | @staticmethod
25 | def _flash2():
26 | if not run('pkill -f boardd'):
27 | error('Error killing boardd! Is it running? (continuing...)')
28 | importlib.import_module('panda', 'Panda').Panda().flash()
29 |
--------------------------------------------------------------------------------
/py_utils/colors.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/python
2 | # -*- coding: utf-8 -*-
3 | class COLORS:
4 | def __init__(self):
5 | self.HEADER = '\033[95m'
6 | self.OKBLUE = '\033[94m'
7 | self.CBLUE = '\33[44m'
8 | self.BOLD = '\033[1m'
9 | self.CITALIC = '\33[3m'
10 | self.OKGREEN = '\033[92m'
11 | self.CWHITE = '\33[37m'
12 | self.ENDC = '\033[0m' + self.CWHITE
13 | self.UNDERLINE = '\033[4m'
14 | self.PINK = '\33[38;5;207m'
15 | self.PRETTY_YELLOW = self.BASE(220)
16 |
17 | self.RED = '\033[91m'
18 | self.PURPLE_BG = '\33[45m'
19 | self.YELLOW = '\033[93m'
20 | self.BLUE_GREEN = self.BASE(85)
21 |
22 | self.FAIL = self.RED
23 | self.INFO = self.PURPLE_BG
24 | self.SUCCESS = self.OKGREEN
25 | self.PROMPT = self.YELLOW
26 | self.DBLUE = '\033[36m'
27 | self.CYAN = self.BASE(39)
28 | self.WARNING = '\033[33m'
29 |
30 | def BASE(self, col): # seems to support more colors
31 | return '\33[38;5;{}m'.format(col)
32 |
33 | def BASEBG(self, col): # seems to support more colors
34 | return '\33[48;5;{}m'.format(col)
35 |
36 |
37 | COLORS = COLORS()
38 |
--------------------------------------------------------------------------------
/emu.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | export COMMUNITY_PATH=/data/community
3 | export COMMUNITY_BASHRC_PATH=${COMMUNITY_PATH}/.bashrc
4 | export OH_MY_COMMA_PATH="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"
5 |
6 | source ${OH_MY_COMMA_PATH}/powerline.sh
7 | source ${OH_MY_COMMA_PATH}/aliases.sh
8 |
9 | function _updateohmycomma(){ # good to keep a backup in case python CLI is broken
10 | source ${OH_MY_COMMA_PATH}/update.sh
11 | source ${OH_MY_COMMA_PATH}/emu.sh
12 | }
13 |
14 | function emu(){ # main wrapper function
15 | if $(python -c 'import sys; print(".".join(map(str, sys.version_info[:3])))' | grep -q -e '^2')
16 | then
17 | python3 ${OH_MY_COMMA_PATH}/emu.py "$@"
18 | else
19 | python ${OH_MY_COMMA_PATH}/emu.py "$@"
20 | fi
21 |
22 | if [ $? = 1 ] && [ "$1" = "update" ]; then # fallback to updating immediately if CLI crashed updating
23 | printf "\033[91mAn error occurred in the Python CLI, attempting to manually update .oh-my-comma...\n"
24 | printf "Press Ctrl+C to cancel!\033[0m\n"
25 | sleep 5
26 | _updateohmycomma
27 | fi
28 | }
29 |
30 | source ${OH_MY_COMMA_PATH}/check-for-updates.sh
31 |
--------------------------------------------------------------------------------
/emu.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/python
2 | # -*- coding: utf-8 -*-
3 | import sys
4 | if __package__ is None:
5 | import sys
6 | from os import path
7 | sys.path.append(path.abspath(path.join(path.dirname(__file__), 'py_utils')))
8 | sys.path.append(path.abspath(path.join(path.dirname(__file__), 'commands')))
9 |
10 | from py_utils.emu_utils import BaseFunctions
11 | from py_utils.emu_utils import OPENPILOT_PATH
12 | from commands import EMU_COMMANDS
13 |
14 | sys.path.append(OPENPILOT_PATH) # for importlib
15 | DEBUG = not path.exists('/data/params/d')
16 |
17 |
18 | class Emu(BaseFunctions):
19 | def __init__(self, args):
20 | self.name = 'emu'
21 | self.args = args
22 | self.commands = {cmd.name: cmd for cmd in EMU_COMMANDS}
23 | self.parse()
24 |
25 | def parse(self):
26 | cmd = self.next_arg()
27 | if cmd is None:
28 | self.print_commands(error_msg='You must specify a command for emu. Some options are:', ascii_art=True)
29 | return
30 | if cmd not in self.commands:
31 | self.print_commands(error_msg='Unknown command! Try one of these:')
32 | return
33 | self.commands[cmd].main(self.args, cmd)
34 |
35 |
36 | if __name__ == "__main__":
37 | args = sys.argv[1:]
38 | if DEBUG:
39 | args = input().split(' ')
40 | if '' in args:
41 | args.remove('')
42 | emu = Emu(args)
43 |
--------------------------------------------------------------------------------
/default-bashrcs/.bashrc-community:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | ## BEGIN .oh-my-comma magic ###
4 |
5 | # If not running interactively, don't do anything
6 | case $- in
7 | *i*) ;;
8 | *) return;;
9 | esac
10 |
11 | # enable color support of ls and also add handy aliases
12 | if [ -x /usr/bin/dircolors ]; then
13 | test -r ~/.dircolors && eval "$(dircolors -b ~/.dircolors)" || eval "$(dircolors -b)"
14 | alias ls='ls --color=auto'
15 | #alias dir='dir --color=auto'
16 | #alias vdir='vdir --color=auto'
17 |
18 | alias grep='grep --color=auto'
19 | alias fgrep='fgrep --color=auto'
20 | alias egrep='egrep --color=auto'
21 | fi
22 |
23 | # colored GCC warnings and errors
24 | export GCC_COLORS='error=01;31:warning=01;35:note=01;36:caret=01;32:locus=01:quote=01'
25 |
26 | source /data/community/.oh-my-comma/emu.sh
27 |
28 | # Change active histfile to a writable directory outside of the /system partition
29 | HISTFILE="/data/community/.bash_history"
30 |
31 | # don't put duplicate lines or lines starting with space in the history.
32 | # See bash(1) for more options
33 | HISTCONTROL=ignoreboth
34 |
35 | # append to the history file, don't overwrite it
36 | shopt -s histappend
37 |
38 | ### End of .oh-my-comma magic ###
39 |
40 | # This is your space to configure your terminal to your liking
41 |
42 | # Change default working directory
43 | # Just in case openpilot is missing, default to /data
44 | cd /data
45 | if [ -d "/data/openpilot" ]; then
46 | cd /data/openpilot
47 | fi
48 |
--------------------------------------------------------------------------------
/update.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | OH_MY_COMMA_PATH=/data/community/.oh-my-comma
3 | PREV_BRANCH=$(cd ${OH_MY_COMMA_PATH} && git rev-parse --abbrev-ref HEAD)
4 | PREV_VERSION=$(cd ${OH_MY_COMMA_PATH} && git describe --tags | grep -Po "^v(\d+\.)?(\d+\.)?(\*|\d+)")
5 | PREV_REMOTE=$(cd ${OH_MY_COMMA_PATH} && git config --get remote.origin.url)
6 | git -C ${OH_MY_COMMA_PATH} pull
7 |
8 | sh ${OH_MY_COMMA_PATH}/install.sh 'update'
9 |
10 | CURRENT_BRANCH=$(cd ${OH_MY_COMMA_PATH} && git rev-parse --abbrev-ref HEAD)
11 | CURRENT_VERSION=$(cd ${OH_MY_COMMA_PATH} && git describe --tags | grep -Po "^v(\d+\.)?(\d+\.)?(\*|\d+)")
12 | CURRENT_REMOTE=$(cd ${OH_MY_COMMA_PATH} && git config --get remote.origin.url)
13 |
14 | echo "Updated ${OH_MY_COMMA_PATH}"
15 | echo "====== FROM ======"
16 | printf "Version \033[92m${PREV_VERSION}\033[0m | branch \033[92m${PREV_BRANCH}\033[0m | remote \033[92m${PREV_REMOTE}\033[0m\n"
17 | printf "Version \033[92m${CURRENT_VERSION}\033[0m | branch \033[92m${CURRENT_BRANCH}\033[0m | remote \033[92m${CURRENT_REMOTE}\033[0m\n"
18 | echo "======= TO ======="
19 |
20 | if git -C $OH_MY_COMMA_PATH log --stat -1 | grep -q 'default-bashrcs/.bashrc-community'; then
21 | if [ "${PREV_VERSION}" != "${CURRENT_VERSION}" ]; then
22 | printf "\n\33[38;5;190mThe default .bashrc has been updated!\033[0m The update has not been applied to retain your custom changes.\nTo update and reset your .bashrc, run the command:\n"
23 | printf "\033[92mcp -fr ${OH_MY_COMMA_PATH}/default-bashrcs/.bashrc-community /data/community/.bashrc\033[0m\n\n"
24 | printf "This will wipe any custom changes you've made!\n"
25 | fi
26 | fi
27 |
--------------------------------------------------------------------------------
/commands/README.md:
--------------------------------------------------------------------------------
1 | # Commands
2 |
3 | #### `emu fork`:
4 | 🍴 Manage installed forks, or install a new one
5 | - `emu fork switch`: 🍴 Switch between any openpilot fork
6 | - Arguments 💢:
7 | - username (optional): 👤 The username of the fork's owner to switch to, will use current fork if not provided
8 | - -b, --branch (optional): 🌿 Branch to switch to, will use fork's default branch if not provided
9 | - *New Behavior:* If a branch is provided with `-b` and username is not supplied, it will use the current fork switched to
10 | - Example 📚:
11 | - `emu fork switch stock devel`
12 | - `emu fork list`: 📜 See a list of installed forks and branches
13 | - Arguments 💢:
14 | - fork (optional): 🌿 See branches of specified fork
15 | - Example 📚:
16 | - `emu fork list stock`
17 |
18 | #### `emu panda`:
19 | 🐼 panda interfacing tools
20 | - `emu panda flash`: 🐼 flashes panda with make recover (usually works with the C2)
21 | - `emu panda flash2`: 🎍 flashes panda using Panda module (usually works with the EON)
22 |
23 | #### `emu debug`:
24 | de-🐛-ing tools
25 | - `emu debug controlsd`: logs controlsd to /data/output.log by default
26 | - Arguments 💢:
27 | - -o, --output: Name of file to save log to
28 | - Example 📚:
29 | - `emu debug controlsd /data/controlsd_log`
30 |
31 | #### `emu device`:
32 | 📈 Statistics about your device
33 | - `emu device battery`: 🔋 see information about the state of your battery
34 | - `emu device reboot`: ⚡ safely reboot your device
35 | - `emu device shutdown`: 🔌 safely shutdown your device
36 | - Arguments 💢:
37 | - -r, --reboot: ♻️ An alternate way to reboot your device
38 | - `emu device settings`: ⚙ open the Settings app
39 | - Arguments 💢:
40 | - -c, --close: ❌ Closes the settings application
41 |
--------------------------------------------------------------------------------
/commands/debug/__init__.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/python
2 | # -*- coding: utf-8 -*-
3 | from commands.base import CommandBase, Command, Flag
4 | from py_utils.colors import COLORS
5 | from py_utils.emu_utils import run, kill, warning, check_output, is_affirmative, error, info, success
6 | from py_utils.emu_utils import OPENPILOT_PATH
7 |
8 |
9 | class Debug(CommandBase):
10 | def __init__(self):
11 | super().__init__()
12 | self.name = 'debug'
13 | self.description = 'de-🐛-ing tools'
14 |
15 | self.commands = {'controlsd': Command(description='🔬 logs controlsd to /data/output.log by default',
16 | flags=[Flag(['-o', '--output'], 'Name of file to save log to', dtype='str')]),
17 | 'reload': Command(description='🔄 kills the current openpilot session and restarts it (all without rebooting)')}
18 |
19 | @staticmethod
20 | def _reload():
21 | info('This will kill the current openpilot tmux session, set up a new one, and relaunch openpilot.')
22 | info('Confirm you would like to continue')
23 | if not is_affirmative():
24 | error('Aborting!')
25 | return
26 |
27 | r = check_output('tmux kill-session -t comma')
28 | if r.success:
29 | info('Killed the current openpilot session')
30 | else:
31 | warning('Error killing current openpilot session, continuing...')
32 |
33 | # Command below thanks to mlp
34 | r = check_output(['tmux', 'new', '-s', 'comma', '-d',
35 | "echo $$ > /dev/cpuset/app/tasks;" # add pid of current shell to app cpuset
36 | "echo $PPID > /dev/cpuset/app/tasks;" # (our parent, tmux, also gets all the cores)
37 | "/data/openpilot/launch_openpilot.sh"])
38 | if r.success:
39 | success('Succesfully started a new tmux session for openpilot!')
40 | success('Type {}tmux a{} to attach to it'.format(COLORS.FAIL, COLORS.SUCCESS))
41 |
42 | def _controlsd(self):
43 | out_file = '/data/output.log'
44 | flags = self.get_flags('controlsd')
45 | if flags.output is not None:
46 | out_file = flags.output
47 | # r = run('pkill -f controlsd') # terminates file for some reason # todo: remove me if not needed
48 | r = kill('selfdrive.controls.controlsd') # seems to work, some process names are weird
49 | if r is None:
50 | warning('controlsd is already dead! (continuing...)')
51 | run('python {}/selfdrive/controls/controlsd.py'.format(OPENPILOT_PATH), out_file=out_file)
52 |
--------------------------------------------------------------------------------
/commands/device/__init__.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/python
2 | # -*- coding: utf-8 -*-
3 | from commands.base import CommandBase, Command, Flag
4 | from py_utils.emu_utils import error, check_output, COLORS, success
5 |
6 |
7 | class Device(CommandBase):
8 | def __init__(self):
9 | super().__init__()
10 | self.name = 'device'
11 | self.description = '📈 Statistics about your device'
12 |
13 | self.commands = {'battery': Command(description='🔋 see information about the state of your battery'),
14 | 'reboot': Command(description='⚡ safely reboot your device'),
15 | 'shutdown': Command(description='🔌 safely shutdown your device',
16 | flags=[Flag(['-r', '--reboot'], 'An alternate way to reboot your device', dtype='bool')]),
17 | 'settings': Command(description='⚙️ open the Settings app',
18 | flags=[Flag(['-c', '--close'], 'Closes the settings application', dtype='bool')])}
19 |
20 | def _settings(self):
21 | flags = self.get_flags('settings')
22 | if flags.close:
23 | check_output('kill $(pgrep com.android.settings)', shell=True)
24 | success('⚙️ Closed settings!')
25 | else:
26 | check_output('am start -a android.settings.SETTINGS')
27 | success('⚙️ Opened settings!')
28 |
29 | def _shutdown(self):
30 | flags = self.get_flags('shutdown')
31 | if flags.reboot:
32 | self._reboot()
33 | return
34 | check_output('am start -n android/com.android.internal.app.ShutdownActivity')
35 | success('🌙 Goodnight!')
36 |
37 | @staticmethod
38 | def _reboot():
39 | check_output('am start -a android.intent.action.REBOOT')
40 | success('👋 See you in a bit!')
41 |
42 | @staticmethod
43 | def _battery():
44 | r = check_output('dumpsys batterymanager')
45 | if not r:
46 | error('Unable to get battery status!')
47 | return
48 | r = r.output.split('\n')
49 | r = [i.strip() for i in r if i != ''][1:]
50 | battery_idxs = {'level': 7, 'temperature': 10}
51 | success('Battery info:')
52 | for name in battery_idxs:
53 | idx = battery_idxs[name]
54 | info = r[idx]
55 |
56 | value = float(info.split(': ')[1])
57 | if name == 'temperature':
58 | value /= 10
59 | value = str(value) + '°C'
60 | else:
61 | value = str(value) + '%'
62 |
63 | value = COLORS.SUCCESS + str(value)
64 | name = COLORS.WARNING + name.title()
65 | print('- {}: {}{}'.format(name, value, COLORS.ENDC))
66 |
--------------------------------------------------------------------------------
/check-for-updates.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | #Based on https://github.com/ohmyzsh/ohmyzsh/blob/master/tools/check_for_upgrade.sh
4 | if [[ "$OMC_DISABLE_AUTO_UPDATE" = true ]] \
5 | || ! command -v git &>/dev/null; then
6 | return
7 | fi
8 | OMC_EPOCH=$(date +%s)
9 | set +x
10 | function current_epoch() {
11 | echo $(($OMC_EPOCH/60/60/24))
12 | }
13 |
14 | function update_last_updated_file() {
15 | echo "LAST_EPOCH=$(current_epoch)" > "${OH_MY_COMMA_PATH}/log/.omc-update"
16 | }
17 |
18 | function omc_delete_update_lock() {
19 | rm -rf '$OH_MY_COMMA_PATH/log/update.lock' || return 1
20 | }
21 | # Remove lock directory if older than a day
22 | if mtime=$(date +%s -r "$OH_MY_COMMA_PATH/log/update.lock" 2>/dev/null); then
23 | if (( (mtime + 3600 * 24) < OMC_EPOCH )); then
24 | command rm -rf "$OH_MY_COMMA_PATH/log/update.lock"
25 | fi
26 | fi
27 |
28 | # Check for lock directory
29 | if ! command mkdir "$OH_MY_COMMA_PATH/log/update.lock" 2>/dev/null; then
30 | return
31 | fi
32 |
33 | # Remove lock directory on exit. `return 1` is important for when trapping a SIGINT:
34 | # The return status from the function is handled specially. If it is zero, the signal is
35 | # assumed to have been handled, and execution continues normally. Otherwise, the shell
36 | # will behave as interrupted except that the return status of the trap is retained.
37 | omc_delete_update_lock
38 |
39 |
40 | # Create or update .omc-update file if missing or malformed
41 | if ! source "${OH_MY_COMMA_PATH}/log/.omc-update" 2>/dev/null || [[ -z "$LAST_EPOCH" ]]; then
42 | touch ${OH_MY_COMMA_PATH}/log/.omc-update
43 | update_last_updated_file
44 | fi
45 |
46 | # Number of days before trying to update again
47 | epoch_target=${OMC_AUTOUPDATE_DAYS:-7}
48 | # Test if enough time has passed until the next update
49 | if (( ( $(current_epoch) - LAST_EPOCH ) < $epoch_target )); then
50 | return
51 | fi
52 |
53 | cd ${OH_MY_COMMA_PATH}
54 |
55 | git fetch
56 | OMC_UPSTREAM=${1:-'@{u}'}
57 | OMC_LOCAL=$(git rev-parse @)
58 | OMC_REMOTE=$(git rev-parse "$OMC_UPSTREAM")
59 |
60 | if [ $OMC_LOCAL != $OMC_REMOTE ]; then
61 | # Ask for confirmation before updating unless disabled
62 | if [[ "$OMC_DISABLE_UPDATE_PROMPT" = true ]]; then
63 | emu update
64 | else
65 | echo "[emu.sh] Current OMC branch:"
66 | echo "$(git branch | head -n 1)"
67 | echo "$(git status | head -n 2 | tail -n 1)"
68 | # input sink to swallow all characters typed before the prompt
69 | # and add a newline if there wasn't one after characters typed
70 | read -r -p "[emu.sh] Update .oh-my-comma? [Y/n] " option
71 | [[ "$option" != $'\n' ]] && echo
72 | case "$option" in
73 | [yY$'\n']) emu update && update_last_updated_file ;;
74 | [nN]) update_last_updated_file ;;
75 | *) emu update ;;
76 | esac
77 | fi
78 | fi
79 | cd -
80 | unset -f current_epoch update_last_updated_file
81 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## comma.ai command-line additions and practical tooling for all
2 |
3 | improving the dev workflow friction is paramount to innovating [openpilot](https://github.com/commaai/openpilot)
4 |
5 | ***PRs accepted!** What cool shit do you do to your ssh session with your car??*
6 |
7 | This tool was created by [Alice Knag](https://github.com/AskAlice) - `@emu#6969` on Discord, and is widely contributed to by [ShaneSmiskol](https://github.com/sshane) - `Shane#6175`
8 | If you have any questions about the development process, or have any ideas you want to see happen, check out [CONTRIBUTING.md](CONTRIBUTING.md) and/or DM one of us on Discord or ask in #custom-forks
9 |
10 |
11 |
12 |
13 |
Click for Full Size
14 |
15 |
16 | # Getting Started
17 |
18 | To install these utilities, SSH into your comma device running neos (ie Comma 2, Eon, etc), and paste in the following:
19 | ```bash
20 | bash <(curl -fsSL install.emu.sh) # the brain of the bird
21 | source /data/community/.bashrc
22 | ```
23 |
24 |
25 |
26 | ---
27 | Read the README for . You can optionally [install the fonts on the computer/terminal emulator that you SSH from](https://github.com/powerline/fonts)
28 |
29 | Alternately, you can install [Nerd Fonts](https://github.com/ryanoasis/nerd-fonts), as it provides more icons than powerline fonts, and is more maintained.
30 |
31 | Once NEOS 15 comes out, zsh will be used and [powerlevel10k](https://github.com/romkatv/powerlevel10k) will be the optimal powerline
32 |
33 | The default directory of your bash/ssh session is now `/data/openpilot`. Much easier to git pull after shelling in.
34 |
35 | # welcome to the family
36 |
37 |
38 |
39 | Emu my neo!
40 | You should now be able to use the `emu` command.
41 |
42 | # Updating
43 |
44 | Once you've installed, you can update via the utility
45 |
46 | ```bash
47 | emu update
48 | ```
49 |
50 | This will essentially perform a git pull and replace all current files in the `/data/community/.oh-my-comma` directory with new ones, if an update is available, as well as check the integrity of the files that must remain elsewhere on the filesystem such as the .bashrc and powerline configs
51 |
52 | # Commands
53 |
54 | ### General
55 | - `emu update`: 🎉 Updates this tool, recommended to restart ssh session
56 | - `emu uninstall`: 👋 Uninstalls emu
57 | ### [Forks](#fork-management)
58 | - `emu fork`: 🍴 Manage installed forks, or install a new one
59 | - `emu fork switch`: 🍴 Switch between any openpilot fork
60 | - `emu fork list`: 📜 See a list of installed forks and branches
61 | ### Panda
62 | - `emu panda`: 🐼 panda interfacing tools
63 | - `emu panda flash`: 🐼 flashes panda with make recover (usually works with the C2)
64 | - `emu panda flash2`: 🎍 flashes panda using Panda module (usually works with the EON)
65 | ### Debugging
66 | - `emu debug`: de-🐛-ing tools
67 | - `emu debug controlsd`: 🔬 logs controlsd to /data/output.log by default
68 | - `emu device`: 📈 Statistics about your device
69 | - `emu device battery`: 🔋 see information about the state of your battery
70 | - `emu device reboot`: ⚡ safely reboot your device
71 | - `emu device shutdown`: 🔌 safely shutdown your device
72 | - `emu device settings`: ⚙ open the Settings app
73 |
74 | To see more information about each command and its arguments, checkout the full [command documentation here.](/commands/README.md)
75 |
76 | ---
77 |
78 | # Fork management
79 | When you first run any `emu fork` command, `emu` will ask you to perform a one-time setup of cloning the base repository of openpilot from commaai. This may take a while, but upon finishing the setup you will be able to switch to any openpilot fork much quicker than the time it usually takes to full-clone a new fork the old fashioned way.
80 |
81 | For each new fork you install with the `emu fork switch` command, Git is able to re-use blobs already downloaded from commaai/openpilot and other similar installed forks, enabling quicker install times. For a shorter command, `fork` is automatically aliased to `emu fork switch`.
82 |
83 | # Git config
84 |
85 | In a rw filesystem, you can edit your git config so you can push your changes up easily.
86 |
87 | ```bash
88 | mount -o rw,remount /system
89 | git config --global user.name "your_username"
90 | git config --global user.email "your_email_address@example.com"
91 | git config --global credential.helper store
92 | git pull
93 | mount -o r,remount /system
94 | ```
95 |
96 | if the git pull fails, just do some action on git that requires authentication, and you should be good to go
97 |
--------------------------------------------------------------------------------
/commands/base.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/python
2 | # -*- coding: utf-8 -*-
3 | from py_utils.colors import COLORS
4 | from py_utils.emu_utils import ArgumentParser, BaseFunctions, success, error
5 |
6 |
7 | class CommandBase(BaseFunctions):
8 | def __init__(self):
9 | self.name = ''
10 | self.commands = {}
11 |
12 | def main(self, args, cmd_name):
13 | self.args = args
14 | cmd = self.next_arg()
15 | if len(self.commands) > 0:
16 | if cmd is None:
17 | self.print_commands(error_msg='You must specify a command for emu {}. Some options are:'.format(cmd_name))
18 | return
19 | if cmd not in self.commands:
20 | self.print_commands(error_msg='Unknown command! Try one of these:')
21 | return
22 | self.start_function_from_str(cmd)
23 | else:
24 | self.start_function_from_str(cmd_name) # eg. update and uninstall
25 |
26 | def start_function_from_str(self, cmd):
27 | cmd = '_' + cmd
28 | if not hasattr(self, cmd):
29 | error('Command has not been implemented yet, please try updating.')
30 | return
31 | getattr(self, cmd)() # call command's function
32 |
33 | def get_flags(self, cmd_name):
34 | try:
35 | return self.commands[cmd_name].parser.parse_args(self.args)
36 | except Exception as e:
37 | error(e)
38 | self._help(cmd_name)
39 | exit(1)
40 |
41 | def _help(self, cmd, show_description=True, leading=''):
42 | has_extra_info = False
43 | description = self.commands[cmd].description
44 | if show_description:
45 | print('{}>> Description 📚: {}{}'.format(COLORS.CYAN, description, COLORS.ENDC))
46 |
47 | flags = self.commands[cmd].flags
48 |
49 | flags_to_print = []
50 | if flags is not None and len(flags) > 0:
51 | has_extra_info = True
52 | usage_req = [f.aliases[0] for f in flags if f.required and len(f.aliases) == 1] # if required
53 | usage_non_req = [f.aliases[0] for f in flags if not f.required and len(f.aliases) == 1] # if non-required non-positional
54 | usage_flags = [f.aliases for f in flags if not f.required and len(f.aliases) > 1 or f.aliases[0].startswith('-')] # if flag
55 | if len(usage_req) > 0 or len(usage_non_req) > 0: # print usage with proper braces
56 | usage_req = ['[{}]'.format(u) for u in usage_req]
57 | usage_non_req = ['({})'.format(u) for u in usage_non_req]
58 | if len(usage_flags):
59 | # formats flags to: "[-b BRANCH, -o OUTPUT]"
60 | usage_flags = ['{} {}'.format(min(u, key=len), max(u, key=len).upper()[2:]) for u in usage_flags]
61 | usage_flags = ['[{}]'.format(', '.join(usage_flags))]
62 |
63 | usage = ['emu', self.name, cmd] + usage_req + usage_non_req + usage_flags
64 | print(leading + COLORS.WARNING + '>> Usage:{} {}'.format(COLORS.OKGREEN, ' '.join(usage)) + COLORS.ENDC)
65 |
66 | print(leading + COLORS.WARNING + '>> Arguments 💢:' + COLORS.ENDC)
67 | for flag in flags:
68 | aliases = COLORS.SUCCESS + ', '.join(flag.aliases) + COLORS.WARNING
69 | if not flag.required and '-' not in aliases:
70 | aliases += COLORS.RED + ' (optional)' + COLORS.WARNING
71 | flags_to_print.append(leading + COLORS.WARNING + ' - {}: {}'.format(aliases, flag.description) + COLORS.ENDC)
72 | print('\n'.join(flags_to_print))
73 |
74 | commands = self.commands[cmd].commands
75 | cmds_to_print = []
76 | if commands is not None and len(commands) > 0:
77 | print(leading + '{}>> Commands 💻:{}'.format(COLORS.OKGREEN, COLORS.ENDC))
78 | for cmd in commands:
79 | cmds_to_print.append(leading + COLORS.FAIL + ' - {}: {}'.format(cmd, success(commands[cmd].description, ret=True)) + COLORS.ENDC)
80 | print('\n'.join(cmds_to_print))
81 | return has_extra_info
82 |
83 |
84 | class Flag:
85 | def __init__(self, aliases, description, required=False, dtype='bool'):
86 | if isinstance(aliases, str):
87 | self.aliases = [aliases]
88 | else:
89 | self.aliases = aliases
90 | self.description = description
91 | self.required = required
92 | self.dtype = dtype
93 | if self.required and self.aliases[0][0] == '-':
94 | raise Exception('Positional arguments cannot be required!')
95 |
96 |
97 | class Command:
98 | def __init__(self, description=None, commands=None, flags=None):
99 | self.parser = ArgumentParser()
100 | self.description = description
101 | self.commands = commands
102 | self.has_flags = False
103 | self.flags = flags
104 | if flags is not None:
105 | self.has_flags = True
106 | for flag in flags:
107 | # for each flag, add it as argument with aliases.
108 | parser_args = {} # handle various conditions
109 | if not flag.required and flag.dtype not in ['bool']:
110 | parser_args['nargs'] = '?'
111 |
112 | if flag.dtype != 'bool':
113 | parser_args['action'] = 'store'
114 | elif flag.dtype == 'bool':
115 | parser_args['action'] = 'store_true'
116 |
117 | if flag.dtype == 'bool': # type bool is not required when store_true
118 | pass
119 | elif flag.dtype == 'str':
120 | parser_args['type'] = str
121 | elif flag.dtype == 'int':
122 | parser_args['type'] = int
123 | else:
124 | error('Unsupported dtype: {}'.format(flag.dtype))
125 | return
126 | self.parser.add_argument(*flag.aliases, help=flag.description, **parser_args)
127 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | Release 0.1.17 (2021-11-17)
2 | =====
3 |
4 | * Support comma three, which has its .bashrc located elsewhere.
5 | * Nicer user-facing installation process
6 | * Clean up installation logic:
7 | * Minimal modification to the system .bashrc file, only one `source` line is appended to the community .bashrc
8 | * Much safer: the previous installer moved the system .bashrc file to a permanently rw partition
9 |
10 | Release 0.1.16 (2021-04-18)
11 | =====
12 |
13 | * Add `emu debug reload` (or simply `reload`) command to restart openpilot without needing to reboot your device.
14 |
15 |
16 | Release 0.1.15 (2021-04-11)
17 | =====
18 |
19 | * Add `repo` flag to `emu fork switch` command: if a repository's name isn't openpilot and isn't a GitHub fork (no name redirection), you can use this option the first time you switch to the fork (remembers URL after that).
20 |
21 |
22 | Release 0.1.14 (2021-04-02)
23 | =====
24 |
25 | * Remember user's GitHub credentials for 1 day (for pushing)
26 | * When `git push`ing, Git will no longer bother you about your local branch's name not matching the remote's
27 | * You need to re-set up fork management for these two improvements
28 | * Fix issue where it would lock user out from `ssh` if a `/openpilot` directory doesn't exist
29 | * You need to apply the changes manually in your `/data/community/.bashrc` file from [this commit](https://github.com/emu-sh/.oh-my-comma/commit/ea67a5960cf3e4aeb93627060ca4ed990a71f595)
30 |
31 |
32 | Release 0.1.13 (2021-02-22)
33 | =====
34 |
35 | * Add flag to `emu device settings -c` to close settings app
36 | * Use most similar remote branch (using difflib) if user types unknown close branch
37 | * Use existing function in CommandBase for getting flags, exits if fails so no need to catch errors in each command that has flags
38 | * Alias `fork` to `emu fork switch`. Ex.: `fork stock -b devel`
39 |
40 |
41 | Release 0.1.12 (2020-10-22)
42 | =====
43 |
44 | * Force reinitializes when submodules detected on the branch we're switching to
45 | * Fixes openpilot not starting when switching away and back to a branch with submodule
46 | * Show what git is doing when switching branches (make checkout verbose)
47 | * Speed up switching by ~300ms by only pruning once every 24 hrs.
48 | * Add `--force` flag to switch command, same as `git checkout -f`
49 | * Fix `shutdown` command happily taking any argument without error, defaulting to shutdown when not `-r`
50 | * Add x emoji (❌) prepended to all errors using error function, makes errors stand out more.
51 |
52 |
53 | Release 0.1.11 (2020-10-05)
54 | =====
55 |
56 | * Don't fetch in new sessions while cloning
57 | * Clean up update screen
58 | * Exception catching:
59 | * When user enters branch for fork switch without the -b flag
60 | * KeyboardInterrupt exception while cloning
61 |
62 | Release 0.1.10 (2020-08-28)
63 | =====
64 |
65 | * On first setup we now rename local branch `release2` to `commaai_release2` (removes dangling `release2` branch after setup and switching to stock)
66 | * We also now add the `release2` branch to `installed_branches` for the fork so `emu fork list` now shows current branch after immediate set up with no switching
67 | * Add `dragonpilot` as an alias to `dragonpilot-community/dragonpilot`
68 |
69 | Release 0.1.9 (2020-08-12)
70 | =====
71 |
72 | * Fix "branch you specified does not exist" error due to deleted remote branches
73 | * Also add a prompt to prune the local branches that have deleted on the remote
74 |
75 | Release 0.1.8 (2020-07-29)
76 | =====
77 |
78 | * Run user's switch command after fork management setup
79 | * More verbose fork switching, shows git output
80 | * Only print newline when more information about command is available to better differentiate between commands
81 |
82 | Release 0.1.7 (2020-07-24)
83 | =====
84 |
85 | * Auto updater will check for updates. This is based on .oh-my-zsh's [check_for_upgrade.sh](https://github.com/ohmyzsh/ohmyzsh/blob/master/tools/check_for_upgrade.sh)
86 |
87 | Release 0.1.6 (2020-07-23)
88 | =====
89 |
90 | * Add device command aliases: battery, settings, shutdown
91 |
92 | Release 0.1.5 (2020-07-22)
93 | =====
94 |
95 | * Make the username argument under `emu fork switch` optional. If not specified, it will use the current fork switched to
96 | * Add `--branch` (`-b`) flag for specifying the branch
97 | * This means you must supply `-b` or `--branch` when switching branches, even when supplying the username:
98 |
99 | Old syntax: emu fork switch another_fork branch
100 | New syntax: emu fork switch another_fork -b branch
101 |
102 | Old syntax: emu fork switch same_fork new_branch
103 | New syntax: emu fork switch -b new_branch
104 |
105 | Release 0.1.4 (2020-07-12)
106 | =====
107 |
108 | * Add `emu device settings` command to open the settings app
109 |
110 | Release 0.1.3 (2020-07-06)
111 | =====
112 |
113 | * Make flags/arguments more robust. Optional non-positional arguments are now supported, as long as they are the last arguments.
114 | * `emu fork switch` and `emu fork list` commands added. Uses one singular git repo and adds remotes of forks so that the time to install a new fork is reduced significantly since git is able to re-use blobs.
115 | * A one-time setup is required when using the fork command, this full clones commaai/openpilot which may take a bit of time on first use.
116 | * Change remote of `origin` to `commaai` so that no additional logic is required. Aliases of stock openpilot are: `['stock', 'commaai', 'origin']`
117 | * Stores all installed forks and forks' branches in `/data/community/forks.json` so that the forks command can easily identify when it needs to track and create a branch or just check it out.
118 | * You should still run `git pull` to make sure you get the latest updates from the fork you're currently switched to.
119 | * Dynamic loading of commands. If a command has an exception loading, it won't crash the CLI. Instead you will see an error when you try to call `emu`
120 |
--------------------------------------------------------------------------------
/install.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/bash
2 |
3 | # _
4 | # -=(')
5 | # ;;
6 | # //
7 | # //
8 | # : '.---.__
9 | # | --_-_)__)
10 | # `.____,'
11 | # \ \ ___ ._ _ _ _ _
12 | # ___\ \ / ._>| ' ' || | |
13 | # ( \ \___.|_|_|_|`___|
14 | # \
15 | # /
16 |
17 | # This is the install script for https://emu.sh/
18 | # Located on git at https://github.com/emu-sh/.oh-my-comma
19 | # To install this, ssh into your comma device and paste:
20 | # bash <(curl -fsSL install.emu.sh) # the brain of the bird
21 | # source $SYSTEM_BASHRC_PATH depending on system
22 |
23 | if [ ! -f /EON ] && [ ! -f /TICI ]; then
24 | echo "Attempting to install on an unsupported platform"
25 | echo "emu only supports comma.ai devices at this time"
26 | exit 1
27 | fi
28 |
29 | SYSTEM_BASHRC_PATH=$([ -f /EON ] && echo "/home/.bashrc" || echo "/etc/bash.bashrc")
30 | COMMUNITY_PATH=/data/community
31 | COMMUNITY_BASHRC_PATH=/data/community/.bashrc
32 | OH_MY_COMMA_PATH=/data/community/.oh-my-comma
33 | GIT_BRANCH_NAME=master
34 | GIT_REMOTE_URL=https://github.com/emu-sh/.oh-my-comma.git
35 | OMC_VERSION=0.1.17
36 |
37 | install_echo() { # only prints if not updating
38 | if [ "$update" != true ]; then
39 | # shellcheck disable=SC2059
40 | printf -- "$1\n"
41 | fi
42 | }
43 |
44 | install_community_bashrc() {
45 | # Copies default-bashrcs/.bashrc-community to /data/community/.bashrc
46 | cp "${OH_MY_COMMA_PATH}/default-bashrcs/.bashrc-community" $COMMUNITY_BASHRC_PATH
47 | chmod 755 ${COMMUNITY_BASHRC_PATH}
48 | echo "✅ Copied ${OH_MY_COMMA_PATH}/default-bashrcs/.bashrc-community to ${COMMUNITY_BASHRC_PATH}"
49 | }
50 |
51 | remount_system() {
52 | # Mounts the correct partition at which each OS's .bashrc is located
53 | writable_str=$([ "$1" = "rw" ] && echo "writable" || echo "read-only")
54 | if [ -f /EON ]; then
55 | permission=$([ "$1" = "ro" ] && echo "r" || echo "rw") # just maps ro to r on EON
56 | install_echo "ℹ️ Remounting /system partition as ${writable_str}"
57 | mount -o "$permission",remount /system || exit 1
58 | else
59 | install_echo "ℹ️ Remounting / partition as ${writable_str}"
60 | sudo mount -o "$1",remount / || exit 1
61 | fi
62 | }
63 |
64 | # System .bashrc should exist
65 | if [ ! -f "$SYSTEM_BASHRC_PATH" ]; then
66 | echo "Your .bashrc file does not exist at ${SYSTEM_BASHRC_PATH}"
67 | exit 1
68 | fi
69 |
70 | update=false
71 | if [ $# -ge 1 ] && [ $1 = "update" ]; then
72 | update=true
73 | fi
74 |
75 | if [ ! -d "/data/community" ]; then
76 | mkdir /data/community
77 | chmod 755 /data/community
78 | fi
79 |
80 | if [ ! -d "$OH_MY_COMMA_PATH" ]; then
81 | echo "Cloning .oh-my-comma"
82 | git clone -b ${GIT_BRANCH_NAME} ${GIT_REMOTE_URL} ${OH_MY_COMMA_PATH}
83 | fi
84 |
85 | # FIXME: figure out how to install pip packages in AGNOS
86 | if [ -f /EON ] && [ ! -x "$(command -v powerline-shell)" ] && [ $update = false ]; then
87 | echo "Do you want to install powerline? [You will also need to install the fonts on your local terminal.]"
88 | read -p "[Y/n] > " choices
89 | case ${choices} in
90 | y|Y ) remount_system rw && pip install powerline-shell && remount_system ro;;
91 | * ) echo "Skipping...";;
92 | esac
93 | fi
94 |
95 | install_echo "ℹ️ Installing emu utilities\n"
96 | # If community .bashrc is already sourced, do nothing, else merely append source line to system .bashrc
97 | if grep -q "$SYSTEM_BASHRC_PATH" -e "source ${COMMUNITY_BASHRC_PATH}"; then
98 | install_echo "✅ Community .bashrc is sourced in system .bashrc, skipping"
99 | else
100 | # Append community .bashrc source onto system .bashrc
101 | remount_system rw
102 | echo "ℹ️ Sourcing community .bashrc in system .bashrc"
103 | msg="\n# automatically added by .oh-my-comma:\nif [ -f ${COMMUNITY_BASHRC_PATH} ]; then\n source ${COMMUNITY_BASHRC_PATH}\nfi\n"
104 | if [ -f /TICI ]; then # need to sudo on AGNOS
105 | printf "$msg" | sudo tee -a "$SYSTEM_BASHRC_PATH" > /dev/null || exit 1
106 | else
107 | printf "$msg" | tee -a "$SYSTEM_BASHRC_PATH" > /dev/null || exit 1
108 | fi
109 | remount_system ro
110 | printf "✅ Success!\n\n"
111 | fi
112 |
113 | # FIXME: not applicable on TICI
114 | if [ -f /EON ]; then
115 | install_echo "Checking /home/.config symlink..."
116 | if [ "$(readlink -f /home/.config/powerline-shell)" != "$OH_MY_COMMA_PATH/.config/powerline-shell" ]; then
117 | remount_system rw # FIXME: do we need /system rw to access /home on NEOS?
118 | echo "Creating a symlink of ${OH_MY_COMMA_PATH}/.config/powerline-shell to /home/.config/powerline-shell"
119 | ln -s ${OH_MY_COMMA_PATH}/.config/powerline-shell /home/.config/powerline-shell
120 | remount_system ro
121 | else
122 | install_echo "Symlink check passed"
123 | fi
124 | fi
125 |
126 | # If community .bashrc file doesn't exist, copy from .bashrc-community
127 | if [ ! -f "$COMMUNITY_BASHRC_PATH" ]; then
128 | echo "ℹ️ Creating your community .bashrc at ${COMMUNITY_BASHRC_PATH}"
129 | install_community_bashrc
130 | elif [ $update = false ]; then
131 | printf "\n❗ A .bashrc file already exists at ${COMMUNITY_BASHRC_PATH}, but you're installing .oh-my.comma\n"
132 | printf "Would you like to overwrite it with the default to make sure it's up to date?\n\n"
133 | read -p "[Y/n]: " overwrite
134 | case ${overwrite} in
135 | n|N ) printf "Skipping...\n";;
136 | * ) install_community_bashrc;;
137 | esac
138 | fi
139 |
140 | touch ${COMMUNITY_PATH}/.bash_history
141 | chmod 775 ${COMMUNITY_PATH}/.bash_history
142 |
143 | printf "\n\033[92m"
144 | if [ $update = true ]; then
145 | echo "✅ Successfully updated emu utilities!"
146 | else
147 | echo "✅ Successfully installed emu utilities!"
148 | fi
149 |
150 | CURRENT_BRANCH=$(cd ${OH_MY_COMMA_PATH} && git rev-parse --abbrev-ref HEAD)
151 | if [ "${CURRENT_BRANCH}" != "master" ]; then
152 | printf "\n❗ \033[0;31mWarning:\033[0m your current .oh-my-comma git branch is %s. If this is unintentional, run:\n\033[92mgit -C /data/community/.oh-my-comma checkout master\033[0m\n\n" "${CURRENT_BRANCH}"
153 | fi
154 |
155 | install_echo "Current version: $OMC_VERSION" # prints in update.sh
156 | if [ $update = false ]; then
157 | printf "\033[0mYou may want to exit out of this bash instance to automatically source emu\n"
158 | fi
159 |
160 | printf "\033[0m\n" # reset color
161 |
162 | if [ $update = false ]; then
163 | set +x
164 | fi
165 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Creative Commons Legal Code
2 |
3 | CC0 1.0 Universal
4 |
5 | CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE
6 | LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN
7 | ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS
8 | INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES
9 | REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS
10 | PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM
11 | THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED
12 | HEREUNDER.
13 |
14 | Statement of Purpose
15 |
16 | The laws of most jurisdictions throughout the world automatically confer
17 | exclusive Copyright and Related Rights (defined below) upon the creator
18 | and subsequent owner(s) (each and all, an "owner") of an original work of
19 | authorship and/or a database (each, a "Work").
20 |
21 | Certain owners wish to permanently relinquish those rights to a Work for
22 | the purpose of contributing to a commons of creative, cultural and
23 | scientific works ("Commons") that the public can reliably and without fear
24 | of later claims of infringement build upon, modify, incorporate in other
25 | works, reuse and redistribute as freely as possible in any form whatsoever
26 | and for any purposes, including without limitation commercial purposes.
27 | These owners may contribute to the Commons to promote the ideal of a free
28 | culture and the further production of creative, cultural and scientific
29 | works, or to gain reputation or greater distribution for their Work in
30 | part through the use and efforts of others.
31 |
32 | For these and/or other purposes and motivations, and without any
33 | expectation of additional consideration or compensation, the person
34 | associating CC0 with a Work (the "Affirmer"), to the extent that he or she
35 | is an owner of Copyright and Related Rights in the Work, voluntarily
36 | elects to apply CC0 to the Work and publicly distribute the Work under its
37 | terms, with knowledge of his or her Copyright and Related Rights in the
38 | Work and the meaning and intended legal effect of CC0 on those rights.
39 |
40 | 1. Copyright and Related Rights. A Work made available under CC0 may be
41 | protected by copyright and related or neighboring rights ("Copyright and
42 | Related Rights"). Copyright and Related Rights include, but are not
43 | limited to, the following:
44 |
45 | i. the right to reproduce, adapt, distribute, perform, display,
46 | communicate, and translate a Work;
47 | ii. moral rights retained by the original author(s) and/or performer(s);
48 | iii. publicity and privacy rights pertaining to a person's image or
49 | likeness depicted in a Work;
50 | iv. rights protecting against unfair competition in regards to a Work,
51 | subject to the limitations in paragraph 4(a), below;
52 | v. rights protecting the extraction, dissemination, use and reuse of data
53 | in a Work;
54 | vi. database rights (such as those arising under Directive 96/9/EC of the
55 | European Parliament and of the Council of 11 March 1996 on the legal
56 | protection of databases, and under any national implementation
57 | thereof, including any amended or successor version of such
58 | directive); and
59 | vii. other similar, equivalent or corresponding rights throughout the
60 | world based on applicable law or treaty, and any national
61 | implementations thereof.
62 |
63 | 2. Waiver. To the greatest extent permitted by, but not in contravention
64 | of, applicable law, Affirmer hereby overtly, fully, permanently,
65 | irrevocably and unconditionally waives, abandons, and surrenders all of
66 | Affirmer's Copyright and Related Rights and associated claims and causes
67 | of action, whether now known or unknown (including existing as well as
68 | future claims and causes of action), in the Work (i) in all territories
69 | worldwide, (ii) for the maximum duration provided by applicable law or
70 | treaty (including future time extensions), (iii) in any current or future
71 | medium and for any number of copies, and (iv) for any purpose whatsoever,
72 | including without limitation commercial, advertising or promotional
73 | purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each
74 | member of the public at large and to the detriment of Affirmer's heirs and
75 | successors, fully intending that such Waiver shall not be subject to
76 | revocation, rescission, cancellation, termination, or any other legal or
77 | equitable action to disrupt the quiet enjoyment of the Work by the public
78 | as contemplated by Affirmer's express Statement of Purpose.
79 |
80 | 3. Public License Fallback. Should any part of the Waiver for any reason
81 | be judged legally invalid or ineffective under applicable law, then the
82 | Waiver shall be preserved to the maximum extent permitted taking into
83 | account Affirmer's express Statement of Purpose. In addition, to the
84 | extent the Waiver is so judged Affirmer hereby grants to each affected
85 | person a royalty-free, non transferable, non sublicensable, non exclusive,
86 | irrevocable and unconditional license to exercise Affirmer's Copyright and
87 | Related Rights in the Work (i) in all territories worldwide, (ii) for the
88 | maximum duration provided by applicable law or treaty (including future
89 | time extensions), (iii) in any current or future medium and for any number
90 | of copies, and (iv) for any purpose whatsoever, including without
91 | limitation commercial, advertising or promotional purposes (the
92 | "License"). The License shall be deemed effective as of the date CC0 was
93 | applied by Affirmer to the Work. Should any part of the License for any
94 | reason be judged legally invalid or ineffective under applicable law, such
95 | partial invalidity or ineffectiveness shall not invalidate the remainder
96 | of the License, and in such case Affirmer hereby affirms that he or she
97 | will not (i) exercise any of his or her remaining Copyright and Related
98 | Rights in the Work or (ii) assert any associated claims and causes of
99 | action with respect to the Work, in either case contrary to Affirmer's
100 | express Statement of Purpose.
101 |
102 | 4. Limitations and Disclaimers.
103 |
104 | a. No trademark or patent rights held by Affirmer are waived, abandoned,
105 | surrendered, licensed or otherwise affected by this document.
106 | b. Affirmer offers the Work as-is and makes no representations or
107 | warranties of any kind concerning the Work, express, implied,
108 | statutory or otherwise, including without limitation warranties of
109 | title, merchantability, fitness for a particular purpose, non
110 | infringement, or the absence of latent or other defects, accuracy, or
111 | the present or absence of errors, whether or not discoverable, all to
112 | the greatest extent permissible under applicable law.
113 | c. Affirmer disclaims responsibility for clearing rights of other persons
114 | that may apply to the Work or any use thereof, including without
115 | limitation any person's Copyright and Related Rights in the Work.
116 | Further, Affirmer disclaims responsibility for obtaining any necessary
117 | consents, permissions or other rights required for any use of the
118 | Work.
119 | d. Affirmer understands and acknowledges that Creative Commons is not a
120 | party to this document and has no duty or obligation with respect to
121 | this CC0 or use of the Work.
122 |
--------------------------------------------------------------------------------
/py_utils/emu_utils.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/python
2 | # -*- coding: utf-8 -*-
3 | import sys
4 | import psutil
5 | import difflib
6 | import argparse
7 | import subprocess
8 | import time
9 |
10 | if __package__ is None:
11 | from os import path
12 |
13 | sys.path.append(path.abspath(path.join(path.dirname(__file__), '../py_utils')))
14 | from py_utils.colors import COLORS
15 | else:
16 | from py_utils.colors import COLORS
17 |
18 | COMMUNITY_PATH = '/data/community'
19 | COMMUNITY_BASHRC_PATH = '/data/community/.bashrc'
20 | OH_MY_COMMA_PATH = '/data/community/.oh-my-comma'
21 | UPDATE_PATH = '{}/update.sh'.format(OH_MY_COMMA_PATH)
22 | UNINSTALL_PATH = '{}/uninstall.sh'.format(OH_MY_COMMA_PATH)
23 | OPENPILOT_PATH = '/data/openpilot'
24 |
25 | FORK_PARAM_PATH = '/data/community/forks.json'
26 |
27 |
28 | class ArgumentParser(argparse.ArgumentParser):
29 | def error(self, message):
30 | raise Exception('error: {}'.format(message))
31 |
32 |
33 | class TimeDebugger:
34 | def __init__(self, convention='s', round_to=4, silent=False):
35 | assert convention in ['s', 'ms'], 'Must be "s" or "ms"!'
36 | self.convention = convention
37 | self.round_to = round_to
38 | self.silent = silent
39 | self.reset(full=True)
40 |
41 | def reset(self, full=False):
42 | self.last_time = time.time()
43 | if full:
44 | self.start_time = self.last_time
45 |
46 | def print(self, msg=None, total=False):
47 | if self.silent:
48 | return
49 | if not total:
50 | elapsed = time.time() - self.last_time
51 | elapsed *= 1000 if self.convention == 'ms' else 1
52 | if msg is not None:
53 | msg = 'Time to {}'.format(msg)
54 | else:
55 | msg = 'Time elapsed'
56 | print('{}: {} {}'.format(msg, round(elapsed, self.round_to), self.convention))
57 | else:
58 | elapsed = time.time() - self.start_time
59 | elapsed *= 1000 if self.convention == 'ms' else 1
60 | print('Total: {} {}'.format(round(elapsed, self.round_to), self.convention))
61 | self.reset(total)
62 |
63 |
64 | class BaseFunctions:
65 | def print_commands(self, error_msg=None, ascii_art=False):
66 | if ascii_art:
67 | print(EMU_ART)
68 |
69 | if error_msg is not None:
70 | error(error_msg)
71 | max_cmd = max([len(_c) for _c in self.commands]) + 1
72 | for idx, cmd in enumerate(self.commands):
73 | desc = COLORS.CYAN + self.commands[cmd].description
74 | print_cmd = '{} {}'.format(self.name, cmd)
75 | if self.name != 'emu':
76 | print_cmd = 'emu {}'.format(print_cmd)
77 | print(COLORS.OKGREEN + ('- {:<%d} {}' % max_cmd).format(print_cmd + ':', desc))
78 | if hasattr(self, '_help'):
79 | # leading is for better differentiating between the different commands
80 | if self._help(cmd, show_description=False, leading=' '):
81 | print() # only add newline when there's more information to sift through
82 | print(COLORS.ENDC, end='')
83 |
84 | def next_arg(self, lower=True, ingest=True):
85 | """
86 | Returns next arg and deletes arg from self.args if ingest=True
87 | :param lower: Returns arg.lower()
88 | :param ingest: Deletes returned arg from self.arg
89 | :return:
90 | """
91 | if len(self.args):
92 | arg = self.args[0]
93 | if lower:
94 | arg = arg.lower()
95 | if ingest:
96 | del self.args[0]
97 | else:
98 | arg = None
99 | return arg
100 |
101 |
102 | def str_sim(a, b):
103 | return difflib.SequenceMatcher(a=a, b=b).ratio()
104 |
105 |
106 | def input_with_options(options, default=None):
107 | """
108 | Takes in a list of options and asks user to make a choice.
109 | The most similar option list index is returned along with the similarity percentage from 0 to 1
110 | """
111 |
112 | user_input = input('[{}]: '.format('/'.join(options))).lower().strip()
113 | if not user_input:
114 | return default, 0.0
115 | sims = [str_sim(i.lower().strip(), user_input) for i in options]
116 | argmax = sims.index(max(sims))
117 | return argmax, sims[argmax]
118 |
119 |
120 | def most_similar(find, options):
121 | sims = [[str_sim(i.lower().strip(), find.lower().strip()), i] for i in options]
122 | sims = sorted(sims, reverse=True)
123 | return [[o[1], o[0]] for o in sims]
124 |
125 |
126 | def check_output(cmd, cwd=None, shell=False):
127 | class Output:
128 | def __init__(self, output='', s=True):
129 | self.output = output
130 | self.success = s
131 |
132 | if isinstance(cmd, str) and not shell:
133 | cmd = cmd.split()
134 | try:
135 | return Output(subprocess.check_output(cmd, cwd=cwd, stderr=subprocess.STDOUT, encoding='utf8', shell=shell))
136 | except subprocess.CalledProcessError as e:
137 | if e.output is None:
138 | return Output(e, s=False) # command failed to execute
139 | return Output(e.output) # command executed but it resulted in error
140 |
141 |
142 | def run(cmd, out_file=None): # todo: return output with same format as check_output, but also output to user (current behavior)
143 | """
144 | If cmd is a string, it is split into a list, otherwise it doesn't modify cmd.
145 | The status is returned, True being success, False for failure
146 | """
147 | if isinstance(cmd, str):
148 | cmd = cmd.split()
149 |
150 | f = None
151 | if isinstance(out_file, str):
152 | f = open(out_file, 'a')
153 |
154 | try:
155 | r = subprocess.call(cmd, stdout=f)
156 | return not r
157 | except (Exception, KeyboardInterrupt) as e:
158 | # print(e)
159 | return False
160 |
161 |
162 | def kill(procname):
163 | for proc in psutil.process_iter():
164 | # check whether the process name matches
165 | if proc.name() == procname:
166 | proc.kill()
167 | return True
168 | return None
169 |
170 |
171 | def is_affirmative():
172 | i = None
173 | print(COLORS.PROMPT, end='')
174 | while i not in ['y', 'n', 'yes', 'no', 'sure', '']:
175 | i = input('[Y/n]: ').lower().strip()
176 | print(COLORS.ENDC)
177 | return i in ['y', 'yes', 'sure', '']
178 |
179 |
180 | def error(msg, end='\n', ret=False, start=''):
181 | """
182 | The following applies to error, warning, and success methods
183 | :param msg: The message to display
184 | :param end: The ending char, default is \n
185 | :param ret: Whether to return the formatted string, or print it
186 | :return: The formatted string if ret is True
187 | """
188 | e = start + '❌ {}{}{}'.format(COLORS.FAIL, msg, COLORS.ENDC)
189 | if ret:
190 | return e
191 | print(e, end=end)
192 |
193 |
194 | def warning(msg, end='\n', ret=False):
195 | w = '{}{}{}'.format(COLORS.PROMPT, msg, COLORS.ENDC)
196 | if ret:
197 | return w
198 | print(w, end=end)
199 |
200 |
201 | def success(msg, end='\n', ret=False):
202 | s = '{}{}{}'.format(COLORS.SUCCESS, msg, COLORS.ENDC)
203 | if ret:
204 | return s
205 | print(s, end=end)
206 |
207 |
208 | def info(msg, end='\n', ret=False):
209 | s = '{}{}{}'.format(COLORS.WARNING, msg, COLORS.ENDC)
210 | if ret:
211 | return s
212 | print(s, end=end)
213 |
214 |
215 | EMU_ART = r""" _
216 | -=(""" + COLORS.RED + """'""" + COLORS.CWHITE + """)
217 | ;;
218 | //
219 | //
220 | : '.---.__
221 | | --_-_)__)
222 | `.____,'
223 | \ \ """ + COLORS.OKGREEN + """ ___ ._ _ _ _ _ """ + COLORS.CWHITE + """
224 | ___\ \ """ + COLORS.OKGREEN + """/ ._>| ' ' || | |""" + COLORS.CWHITE + """
225 | ( \ """ + COLORS.OKGREEN + """\___.|_|_|_|`___|""" + COLORS.CWHITE + """
226 | \
227 | /""" + '\n'
228 |
--------------------------------------------------------------------------------
/commands/fork/__init__.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/python
2 | # -*- coding: utf-8 -*-
3 | import shutil
4 | import os
5 | import json
6 | from datetime import datetime
7 | from commands.base import CommandBase, Command, Flag
8 | from py_utils.emu_utils import run, error, success, warning, info, is_affirmative, check_output, most_similar
9 | from py_utils.emu_utils import OPENPILOT_PATH, FORK_PARAM_PATH, COLORS, OH_MY_COMMA_PATH
10 |
11 | GIT_OPENPILOT_URL = 'https://github.com/commaai/openpilot'
12 | REMOTE_ALREADY_EXISTS = 'already exists'
13 | DEFAULT_BRANCH_START = 'HEAD branch: '
14 | REMOTE_BRANCHES_START = 'Remote branches:\n'
15 | REMOTE_BRANCH_START = 'Remote branch:'
16 | DEFAULT_REPO_NAME = 'openpilot'
17 |
18 |
19 | def valid_fork_url(url):
20 | import urllib.request
21 | try:
22 | request = urllib.request.Request(url)
23 | request.get_method = lambda: 'HEAD'
24 | urllib.request.urlopen(request)
25 | return True
26 | except Exception as e:
27 | return False
28 |
29 |
30 | class ForkParams:
31 | def __init__(self):
32 | self.default_params = {'current_fork': None,
33 | 'current_branch': None,
34 | 'installed_forks': {},
35 | 'last_prune': None,
36 | 'setup_complete': False}
37 | self._init()
38 |
39 | def _init(self):
40 | if os.path.exists(FORK_PARAM_PATH):
41 | try:
42 | self._read()
43 | for param in self.default_params:
44 | if param not in self.params:
45 | self.params[param] = self.default_params[param]
46 | return
47 | except:
48 | pass
49 |
50 | self.params = self.default_params # default params
51 | self._write() # failed to read, just write default
52 |
53 | def get(self, key):
54 | return self.params[key]
55 |
56 | def put(self, key, value):
57 | self.params.update({key: value})
58 | self._write()
59 |
60 | def reset(self):
61 | self.params = self.default_params
62 | self._write()
63 |
64 | def _read(self):
65 | with open(FORK_PARAM_PATH, "r") as f:
66 | self.params = json.loads(f.read())
67 |
68 | def _write(self):
69 | with open(FORK_PARAM_PATH, "w") as f:
70 | f.write(json.dumps(self.params, indent=2))
71 |
72 |
73 | class RemoteInfo:
74 | def __init__(self, fork_name, username_aliases, default_branch):
75 | self.username = None # to be added by Fork.__get_remote_info function
76 | self.fork_name = fork_name
77 | self.username_aliases = username_aliases
78 | self.default_branch = default_branch
79 |
80 |
81 | class Fork(CommandBase):
82 | def __init__(self):
83 | super().__init__()
84 | self.name = 'fork'
85 | self.description = '🍴 Manage installed forks, or install a new one'
86 |
87 | self.fork_params = ForkParams()
88 | self.remote_defaults = {'commaai': RemoteInfo('openpilot', ['stock', 'origin'], 'release2'),
89 | 'dragonpilot-community': RemoteInfo('dragonpilot', ['dragonpilot'], 'devel-i18n')} # devel-i18n isn't most stable, but its name remains the same
90 |
91 | self.comma_origin_name = 'commaai'
92 | self.comma_default_branch = self.remote_defaults['commaai'].default_branch
93 |
94 | self.commands = {'switch': Command(description='🍴 Switch between any openpilot fork',
95 | flags=[Flag('username', '👤 The username of the fork\'s owner to switch to, will use current fork if not provided', required=False, dtype='str'),
96 | Flag(['-b', '--branch'], '🌿 Branch to switch to, will use default branch if not provided', required=False, dtype='str'),
97 | Flag(['-r', '--repo'], 'The repository name of the fork, if its name isn\'t openpilot', required=False, dtype='str'),
98 | Flag(['-f', '--force'], '💪 Similar to checkout -f, force checks out new branch overwriting any changes')]),
99 | 'list': Command(description='📜 See a list of installed forks and branches',
100 | flags=[Flag('fork', '🌿 See branches of specified fork', dtype='str')])}
101 |
102 | def _list(self):
103 | if not self._init():
104 | return
105 | flags = self.get_flags('list')
106 | specified_fork = flags.fork
107 |
108 | installed_forks = self.fork_params.get('installed_forks')
109 | if specified_fork is None:
110 | max_branches = 4 # max branches to display per fork when listing all forks
111 | success('Installed forks:')
112 | for idi, fork in enumerate(installed_forks):
113 | print('- {}{}{}'.format(COLORS.OKBLUE, fork, COLORS.ENDC), end='')
114 | current_fork = self.fork_params.get('current_fork')
115 | if current_fork == fork:
116 | print(' (current)')
117 | else:
118 | print()
119 | branches = installed_forks[fork]['installed_branches']
120 | current_branch = self.fork_params.get('current_branch')
121 | if current_branch in branches:
122 | branches.remove(current_branch)
123 | branches.insert(0, current_branch) # move cur_branch to beginning
124 |
125 | if len(branches) > 0:
126 | success(' Branches:')
127 | for idx, branch in enumerate(branches):
128 | if idx < max_branches:
129 | print(' - {}{}{}'.format(COLORS.RED, branch, COLORS.ENDC), end='')
130 | if branch == current_branch and fork == current_fork:
131 | print(' (current)')
132 | else:
133 | print()
134 | else:
135 | print(' - {}...see more branches: {}emu fork list {}{}'.format(COLORS.RED, COLORS.CYAN, fork, COLORS.ENDC))
136 | break
137 | print()
138 | else:
139 | specified_fork = specified_fork.lower()
140 | remote_info = self.__get_remote_info(specified_fork)
141 | if remote_info is not None: # there's an overriding default username available
142 | specified_fork = remote_info.username
143 | if specified_fork not in installed_forks:
144 | error('{} not an installed fork! Try installing it with the {}switch{} command'.format(specified_fork, COLORS.CYAN, COLORS.RED))
145 | return
146 | installed_branches = installed_forks[specified_fork]['installed_branches']
147 | success('Installed branches for {}:'.format(specified_fork))
148 | for branch in installed_branches:
149 | print(' - {}{}{}'.format(COLORS.RED, branch, COLORS.ENDC))
150 |
151 | def _switch(self):
152 | if not self._init():
153 | return
154 | flags = self.get_flags('switch')
155 | if flags.username is flags.branch is None: # since both are non-required we need custom logic to check user supplied sufficient args/flags
156 | error('You must supply either username or branch or both')
157 | self._help('switch')
158 | return
159 |
160 | username = flags.username
161 | branch = flags.branch
162 | repo_name = flags.repo
163 | force_switch = flags.force
164 | if username is None: # branch is specified, so use current checked out fork/username
165 | _current_fork = self.fork_params.get('current_fork')
166 | if _current_fork is not None: # ...if available
167 | info('Assuming current fork for username: {}'.format(COLORS.SUCCESS + _current_fork + COLORS.ENDC))
168 | username = _current_fork
169 | else:
170 | error('Current fork is unknown, please switch to a fork first before switching between branches!')
171 | return
172 |
173 | username = username.lower()
174 | remote_info = self.__get_remote_info(username)
175 | if remote_info is not None: # user entered an alias (ex. stock, dragonpilot)
176 | username = remote_info.username
177 |
178 | installed_forks = self.fork_params.get('installed_forks')
179 | fork_in_params = True
180 | if username not in installed_forks:
181 | fork_in_params = False
182 | if remote_info is not None:
183 | remote_url = f'https://github.com/{username}/{remote_info.fork_name}' # dragonpilot doesn't have a GH redirect
184 | else: # for most forks, GH will redirect from /openpilot if user renames fork
185 | if repo_name is None:
186 | repo_name = DEFAULT_REPO_NAME # openpilot
187 | remote_url = f'https://github.com/{username}/{repo_name}'
188 |
189 | if not valid_fork_url(remote_url):
190 | error('Invalid username{}! {} does not exist'.format('' if flags.repo is None else ' or repository name', remote_url))
191 | return
192 |
193 | r = check_output(['git', '-C', OPENPILOT_PATH, 'remote', 'add', username, remote_url])
194 | if r.success and r.output == '':
195 | success('Remote added successfully!')
196 | elif r.success and REMOTE_ALREADY_EXISTS in r.output:
197 | # remote already added, update params
198 | info('Fork exists but wasn\'t in params, updating now...')
199 | self.__add_fork(username)
200 | else:
201 | error(r.output)
202 | return
203 |
204 | # fork has been added as a remote, switch to it
205 | if fork_in_params:
206 | info('Fetching {}\'s latest changes...'.format(COLORS.SUCCESS + username + COLORS.WARNING))
207 | else:
208 | info('Fetching {}\'s fork, this may take a sec...'.format(COLORS.SUCCESS + username + COLORS.WARNING))
209 |
210 | r = run(['git', '-C', OPENPILOT_PATH, 'fetch', username])
211 | if not r:
212 | error('Error while fetching remote, please try again')
213 | return
214 |
215 | self.__add_fork(username)
216 | self.__prune_remote_branches(username)
217 | r = check_output(['git', '-C', OPENPILOT_PATH, 'remote', 'show', username])
218 | remote_branches, default_remote_branch = self.__get_remote_branches(r)
219 | if remote_branches is None:
220 | return
221 |
222 | if DEFAULT_BRANCH_START not in r.output:
223 | error('Error: Cannot find default branch from fork!')
224 | return
225 |
226 | if branch is None: # user hasn't specified a branch, use remote's default branch
227 | if remote_info is not None: # there's an overriding default branch specified
228 | remote_branch = remote_info.default_branch
229 | local_branch = '{}_{}'.format(remote_info.username, remote_branch)
230 | else:
231 | remote_branch = default_remote_branch # for command to checkout correct branch from remote, branch is previously None since user didn't specify
232 | local_branch = '{}_{}'.format(username, default_remote_branch)
233 | else:
234 | if branch not in remote_branches:
235 | close_branches = most_similar(branch, remote_branches) # remote_branches is gauranteed to have at least 1 branch
236 | if close_branches[0][1] > 0.5:
237 | branch = close_branches[0][0]
238 | info('Unknown branch, checking out most similar: {}'.format(COLORS.SUCCESS + branch + COLORS.WARNING))
239 | else:
240 | error('The branch you specified does not exist!')
241 | self.__show_similar_branches(branch, remote_branches) # if possible
242 | return
243 | remote_branch = branch # branch is now gauranteed to be in remote_branches
244 | local_branch = f'{username}_{branch}'
245 |
246 | # checkout remote branch and prepend username so we can have multiple forks with same branch names locally
247 | if remote_branch not in installed_forks[username]['installed_branches']:
248 | info('New branch! Tracking and checking out {} from {}'.format(local_branch, f'{username}/{remote_branch}'))
249 | command = ['git', '-C', OPENPILOT_PATH, 'checkout', '--track', '-b', local_branch, f'{username}/{remote_branch}']
250 | else: # already installed branch, checking out fork_branch from f'{username}/{branch}'
251 | command = ['git', '-C', OPENPILOT_PATH, 'checkout', local_branch]
252 |
253 | if force_switch:
254 | command.append('-f')
255 | r = run(command)
256 | if not r:
257 | error('Error while checking out branch, please try again or use flag --force')
258 | return
259 | self.__add_branch(username, remote_branch) # we can deduce fork branch from username and original branch f({username}_{branch})
260 |
261 | # reset to remote/branch just to ensure we checked out fully. if remote branch has been force pushed, this will also reset local to remote
262 | r = check_output(['git', '-C', OPENPILOT_PATH, 'reset', '--hard', f'{username}/{remote_branch}'])
263 | if not r.success:
264 | error(r.output)
265 | return
266 |
267 | reinit_subs = self.__init_submodules()
268 | self.fork_params.put('current_fork', username)
269 | self.fork_params.put('current_branch', remote_branch)
270 | info('\n✅ Successfully checked out {}/{} as {}'.format(COLORS.SUCCESS + username, remote_branch + COLORS.WARNING, COLORS.SUCCESS + local_branch))
271 | if reinit_subs:
272 | success('✅ Successfully reinitialized submodules!')
273 |
274 | def __add_fork(self, username, branch=None):
275 | installed_forks = self.fork_params.get('installed_forks')
276 | if username not in installed_forks:
277 | installed_forks[username] = {'installed_branches': []}
278 | if branch is not None:
279 | installed_forks[username]['installed_branches'].append(branch)
280 | self.fork_params.put('installed_forks', installed_forks)
281 |
282 | def __add_branch(self, username, branch): # assumes fork exists in params, doesn't add branch if exists
283 | installed_forks = self.fork_params.get('installed_forks')
284 | if branch not in installed_forks[username]['installed_branches']:
285 | installed_forks[username]['installed_branches'].append(branch)
286 | self.fork_params.put('installed_forks', installed_forks)
287 |
288 | @staticmethod
289 | def __show_similar_branches(branch, branches):
290 | if len(branches) > 0:
291 | info('Did you mean:')
292 | close_branches = most_similar(branch, branches)[:5]
293 | for idx in range(len(close_branches)):
294 | cb = close_branches[idx][0]
295 | if idx == 0:
296 | cb = COLORS.OKGREEN + cb
297 | else:
298 | cb = COLORS.CYAN + cb
299 | print(' - {}{}'.format(cb, COLORS.ENDC))
300 |
301 | def __prune_remote_branches(self, username): # remove deleted remote branches locally
302 | last_prune = self.fork_params.get('last_prune')
303 | if isinstance(last_prune, str) and datetime.now().strftime("%d") == last_prune:
304 | return
305 | self.fork_params.put('last_prune', datetime.now().strftime("%d"))
306 |
307 | r = check_output(['git', '-C', OPENPILOT_PATH, 'remote', 'prune', username, '--dry-run'])
308 | if r.output == '': # nothing to prune
309 | return
310 | branches_to_prune = [b.strip() for b in r.output.split('\n') if 'would prune' in b]
311 | branches_to_prune = [b[b.index(username):] for b in branches_to_prune]
312 |
313 | error('Deleted remote branches detected:', start='\n')
314 | for b in branches_to_prune:
315 | print(COLORS.CYAN + ' - {}'.format(b) + COLORS.ENDC)
316 | warning('\nWould you like to delete them locally?')
317 | if is_affirmative():
318 | r = check_output(['git', '-C', OPENPILOT_PATH, 'remote', 'prune', username])
319 | if r.success:
320 | success('Pruned local branches successfully!')
321 | else:
322 | error('Please try again, something went wrong:')
323 | print(r.output)
324 |
325 | def __get_remote_info(self, username):
326 | for default_username in self.remote_defaults:
327 | remote_info = self.remote_defaults[default_username]
328 | remote_info.username = default_username # add dict key to class instance so we don't have to return a tuple
329 | remote_info.username_aliases.append(default_username) # so default branch works when user enters the actual name
330 | if username in remote_info.username_aliases:
331 | return remote_info
332 | return None
333 |
334 | @staticmethod
335 | def __get_remote_branches(r):
336 | # get remote's branches to verify from output of command in parent function
337 | if not r.success:
338 | error(r.output)
339 | return None, None
340 | if REMOTE_BRANCHES_START in r.output:
341 | start_remote_branches = r.output.index(REMOTE_BRANCHES_START)
342 | remote_branches_txt = r.output[start_remote_branches + len(REMOTE_BRANCHES_START):].split('\n')
343 | remote_branches = []
344 | for b in remote_branches_txt:
345 | b = b.replace('tracked', '').strip()
346 | if 'stale' in b: # support stale/to-be-pruned branches
347 | b = b.split(' ')[0].split('/')[-1]
348 | if ' ' in b or b == '': # end of branches
349 | break
350 | remote_branches.append(b)
351 | elif REMOTE_BRANCH_START in r.output: # remote has single branch, shouldn't need to handle stale here
352 | start_remote_branch = r.output.index(REMOTE_BRANCH_START)
353 | remote_branches = r.output[start_remote_branch + len(REMOTE_BRANCH_START):].split('\n')
354 | remote_branches = [b.replace('tracked', '').strip() for b in remote_branches if b.strip() != '' and 'tracked' in b]
355 | else:
356 | error('Unable to parse remote branches!')
357 | return None, None
358 |
359 | if len(remote_branches) == 0:
360 | error('Error getting remote branches!')
361 | return None, None
362 |
363 | start_default_branch = r.output.index(DEFAULT_BRANCH_START) # get default branch to return
364 | default_branch = r.output[start_default_branch + len(DEFAULT_BRANCH_START):]
365 | end_default_branch = default_branch.index('\n')
366 | default_branch = default_branch[:end_default_branch]
367 | return remote_branches, default_branch
368 |
369 | @staticmethod
370 | def __init_submodules():
371 | r = check_output(['git', '-C', OPENPILOT_PATH, 'submodule', 'status'])
372 | if len(r.output):
373 | info('Submodules detected, reinitializing!')
374 | r0 = check_output(['git', '-C', OPENPILOT_PATH, 'submodule', 'deinit', '--all', '-f'])
375 | r1 = check_output(['git', '-C', OPENPILOT_PATH, 'submodule', 'update', '--init', '--recursive'])
376 | if not r0.success or not r1.success:
377 | error('Error reinitializing submodules for this branch!')
378 | else:
379 | return True
380 | return False
381 |
382 | def _init(self):
383 | if os.path.isdir('/data/community/forks'):
384 | shutil.rmtree('/data/community/forks') # remove to save space
385 | if self.fork_params.get('setup_complete'):
386 | if os.path.exists(OPENPILOT_PATH):
387 | r = check_output(['git', '-C', OPENPILOT_PATH, 'remote', 'show'])
388 | if self.comma_origin_name in r.output.split('\n'): # sign that we're set up correctly todo: check all forks exist as remotes
389 | return True
390 | self.fork_params.put('setup_complete', False) # renamed origin -> commaai does not exist, restart setup
391 | self.fork_params.reset()
392 | warning('There was an error with your clone of commaai/openpilot, restarting initialization!')
393 |
394 | info('To set up emu fork management we will clone commaai/openpilot into {}'.format(OPENPILOT_PATH))
395 | info('Confirm you would like to continue')
396 | if not is_affirmative():
397 | error('Stopping initialization!')
398 | return
399 |
400 | # backup openpilot here to free up /data/openpilot
401 | if os.path.exists(OPENPILOT_PATH):
402 | bak_dir = '{}.bak'.format(OPENPILOT_PATH)
403 | idx = 0
404 | while os.path.exists(bak_dir):
405 | bak_dir = '{}{}'.format(bak_dir, idx)
406 | idx += 1
407 | shutil.move(OPENPILOT_PATH, bak_dir)
408 | success('Backed up your current openpilot install to {}'.format(bak_dir))
409 |
410 | info('Cloning commaai/openpilot into {}, please wait...'.format(OPENPILOT_PATH))
411 | r = run(['git', 'clone', '-b', self.comma_default_branch, GIT_OPENPILOT_URL, OPENPILOT_PATH]) # default to stock/release2 for setup
412 | if not r:
413 | error('Error while cloning, please try again')
414 | return
415 |
416 | # rename origin to commaai so it's easy to switch to stock without any extra logic for url checking, etc
417 | r = check_output(['git', '-C', OPENPILOT_PATH, 'remote', 'rename', 'origin', self.comma_origin_name])
418 | if not r.success:
419 | error(r.output)
420 | return
421 |
422 | # rename release2 to commaai_release2 to align with emu fork standards
423 | r = check_output(['git', '-C', OPENPILOT_PATH, 'branch', '-m', f'{self.comma_origin_name}_{self.comma_default_branch}'])
424 | if not r.success:
425 | error(r.output)
426 | return
427 |
428 | # set git config push.default to `upstream` to remove differently named remote branch warning when pushing
429 | check_output(['git', '-C', OPENPILOT_PATH, 'config', 'push.default', 'upstream']) # not game breaking if this fails
430 |
431 | # remember username and password of user for pushing
432 | check_output(['git', '-C', OPENPILOT_PATH, 'config', 'credential.helper', 'cache --timeout=1440']) # cache for a day
433 |
434 | success('Fork management set up successfully! You\'re on {}/{}'.format(self.comma_origin_name, self.comma_default_branch))
435 | success('To get started, try running: {}emu fork switch (username) [-b BRANCH]{}'.format(COLORS.RED, COLORS.ENDC))
436 | self.__add_fork(self.comma_origin_name, self.comma_default_branch)
437 | self.fork_params.put('setup_complete', True)
438 | self.fork_params.put('current_fork', self.comma_origin_name)
439 | self.fork_params.put('current_branch', self.comma_default_branch)
440 | return True
441 |
--------------------------------------------------------------------------------