ppcompile icon indicating copy to clipboard operation
ppcompile copied to clipboard

🏓ping-pong compile, an Emacs package for coding locally but compiling remotely

This Emacs package tries to ease the development workflow that consists of coding locally, compiling remotely, and then fixing errors with =next-error= locally, which is the typical development cycle for C/C++ projects.

~M-x ppcompile~ works in 2 steps under the hood, like playing a ping-pong game:

  1. ping: =rsync= current project to a remote machine, compiles it there,

  2. pong: send the compilation result back from the remote machine, convert remote paths in the =compilation= output to local paths according to the alist =ppcompile-path-mapping-list=

    Then =compilation-mode= can find the files containing errors or warnings correctly while executing =M-x next-error= and =M-x previous-error=.

#+begin_src artist

             +---------+                                          +--------+
             |         |          ping: rsync files               |        |
             |         | ---------------------------------------> |        |
             |  local  |                                          | remote |
             | (Emacs) | <--------------------------------------- | (build)|
             |         |          pong: compile & replace paths   |        |
             +---------+                                          +--------+

#+end_src

The project root is detected via =(project-current)=, if it fails =ppcompile= will take the top-level =git= directory as the project root.

Currently, it's only tested with GNU Emacs 26.3/27.2 on Linux.

  • Installation

[[https://melpa.org/#/ppcompile][file:https://melpa.org/packages/ppcompile-badge.svg]]

ppcompile is now available on [[https://melpa.org/#/ppcompile][melpa]], so you can easily install it by ~M-x package-install RET ppcompile RET~.

** Extra Dependencies

  • built-in packages: =compile=, =auth-source=, =project=, and =files-x=

  • external programs: =rsync=, =ssh=, =expect=.

    Make sure you have these on your system.

    It will not use =expect= if =expect= isn't available on your system, then SSH public key authentication is mandatory in this case.

  • How to Use
  • =M-x ppcompile-config-project= to configure various settings for a project.

    See details in the section [[*Configurations][Configurations]] below.

  • =M-x ppcompile= to ping-pong compile the current project.

    It rsync's the current project to the remote host, compiles it remotely, takes back the output and converts the remote paths to the local ones, so that =compilation-mode= works perfectly. Or put it in another way, it executes =ppcompile-ping= and =ppcompile-pong= sequentially.

    It respects the =compilation-read-command= variable of =compile=, which means it will give you a chance to edit the compile command if you set it to non-=nil=.

    The input history is kept in =ppcompile--remote-command-history=. Hit ENTER directly and it will use the last compile command. You can also hit =M-n= to get the default compile command (on the remote machine), and hit more =M-n= and =M-p= to navigate the command history.

  • =M-x ppcompile-ping= to rsync the project.

  • =M-x ppcompile-pong= to compile the project remotely.

    If you want to change the prompt behavior temporarily, prefix the command, i.e. =C-u M-x ppcompile-pong=.

  • =M-x ppcompile-toggle-debug= to toggle debugging.

  • =M-x ppcompile-get-ssh-password= to get the password of the current project, if password authentication is used, this command is intended for debugging.

  • Configurations

There is quite some configurations to set, globally or per project.

** Per-project Configurations

=M-x ppcompile-config-project= will guide you through to set them up in =.dir-locals.el= in the project root. It makes life a little bit easier if you have more than one remote hosts to compile different projects, which often is the case. You can then fine-tune the =.dir-locals.el= file after finishing the command.

Here is an example of =.dir-locals.el=: #+begin_src elisp ;;; Directory Local Variables ;;; For more information see (info "(emacs) Directory Variables")

((nil (eval . (setq ppcompile-path-mapping-alist `(("/tmp/" . ,(file-name-directory (directory-file-name (file-name-directory (buffer-file-name)))))))) (ppcompile-remote-compile-command . "make -C .") (ppcompile-ssh-user . "test") (ppcompile-ssh-host . "localhost") (ppcompile-rsync-dst-dir . "/tmp/"))) #+end_src

The input history is kept in the variable =ppcompile--config-history=, so you can hit =M-n= and =M-p= to get your previous input to save you some effort.

The configurations covered by the command include:

  • =ppcompile-ssh-host= :: the remote host

  • =ppcompile-ssh-port= :: the ssh port of remote host, which defaults to 22

  • =ppcompile-ssh-user= :: user name, which defaults to currently logged in user, as returned by =(user-login-name)=

  • =ppcompile-rsync-dst-dir= :: remote containing directory for the project

  • =ppcompile-remote-compile-command= :: The compile command executed under the remote project directory.

  • =ppcompile-path-mapping-list= :: alist for path mapping

    The =car= of each whose element is a remote path, and the =cdr= a local path, all paths should be absolute paths.

The above configurations may vary from projects to projects, so they are typically set per project.

** Global Configurations

There are also other global configurations, which have defaults:

  • =ppcompile-ssh-additional-args= :: additional arguments for the =ssh= command line

  • =ppcompile-rsync-additional-args= :: additional arguments for the =rsync= command line

  • =ppcompile-rsync-exclude-list= :: a list specifying files you want to exclude, such as binary files.

  • =ppcompile-ssh-executable= :: The =ssh= executable

  • =ppcompile-rsync-executable= :: The =rsync= executable

  • =ppcompile-expect-executable= :: The =expect= executable

  • =ppcompile-with-password-script-path= :: The path of the helper expect script =with-password.exp=.

    The default value may be wrong if your =.elc= file isn't in the same directory of the =with-password.exp=, which means the file path doesn't exist, to make SSH public key authentication mandatory.

** SSH Credentials Besides that, you may need to configure your passwords in some =auth-source= backends, for example, one entry for a host in =~/.authinfo= looks like: #+begin_src machine localhost port 22 login try password 1 #+end_src

Also, pay attention to Emacs variable =auth-sources= to include your setting.

That being said, public key authentication is recommended though, whenever it's possible, and keep various configurations including identity files in =~/.ssh/config=. ([[https://whatacold.github.io/2019-12-22-manage-ssh-connections-with-ssh-config.html][Manage SSH connections with =~/.ssh/config=]])

  • Troubleshooting

After the above settings, chances are that it still doesn't work. You can troubleshoot it by following these steps:

  1. Turn on debugging by =M-x ppcompile-toggle-debug=

    Re-run it once again, and check out the shell commands in the =Message= buffer, and if there is setting wrong. Run the command on a terminal manually, to see if there is more error info.

  2. Confirm that the password is right by =M-x ppcompile-get-ssh-password= if you're using password authentication for ssh.

    Setting =auth-source= can be tricky, so this may help. Also try ~M-x auth-source-forget-all-cached~ if you just changed your auth-source entries.

Note that these commands should be executed on the buffers of project files, to take advantage of the configurations for that particular project.

  • Other Solutions
  • [[https://github.com/libfuse/sshfs][sshfs]] mounts the remote FS locally, which would be an option if you have a stable, fast network and want to edit remote files just like locally.

    Note that you still need to compile it on the remote host, though you can edit it within your local environment.

  • [[https://github.com/buildfoundation/mainframer][mainframer]], a tool for remote builds, although not based on Emacs, is a more general solution with a similar idea.

  • Development
  • Run =make test= to test the code

    And =make test-with-sshd= to test the functionality with a sshd server, which requires some additional setup:

    1. Start a ssh server at port 22000: =/usr/sbin/sshd -p 22000=

    2. Copy the public key file: =ssh-copy-id -p 22000 -i ./test/id_ppcompile_test localhost= This will append the public key file to =~/.ssh/authorized_keys=, so don't do this on your publicly available server, because it will be open to anyone who uses the private key in the =test/= directory to ssh into your server, and do something evil.

  • =make checkdoc= checks the docstrings.

  • =make compile= compiles the elisp files.

  • Final words

This was my first time to roll out a package seriously, I believe there is much to improve, so pull requests and issues are very welcome.