Skip to content

Commit

Permalink
external tool documentation preliminary draft
Browse files Browse the repository at this point in the history
  • Loading branch information
NoxNovus committed Nov 7, 2024
1 parent a411c31 commit 7ef7154
Show file tree
Hide file tree
Showing 2 changed files with 211 additions and 5 deletions.
19 changes: 14 additions & 5 deletions doc/adding_a_component.md
Original file line number Diff line number Diff line change
@@ -1,20 +1,25 @@
# Adding Component To Odyssey Documentation
# Adding a component to Odyssey

The purpose of this documentation is to explain how to add a component to Odyssey's interface.

This documentation assumes that you have the repo set up locally, that you have installed all necessary dependencies, and that you can compile, build, and then run Odyssey on your local machine.

The codebase makes heavy use of [React](https://react.dev/), which you should be familiar with before constructing a component. There exist tutorials [for more modern React](https://react.dev/learn) as well as [this older React tutorial](
https://legacy.reactjs.org/tutorial/tutorial.html).
The codebase makes heavy use of [React](https://react.dev/), which you should be familiar with before constructing a component. There exist tutorials [for more modern React](https://react.dev/learn) as well as [this older React tutorial](https://legacy.reactjs.org/tutorial/tutorial.html).

TypeScript is the language of choice for this codebase. It features a fairly powerful [type system](https://www.typescriptlang.org/docs/handbook/intro.html) that you should be familiar with.

## Creating a branch

To avoid breaking functionality on the main branch, you should [develop on a separate branch](https://git-scm.com/book/en/v2/Git-Branching-Basic-Branching-and-Merging), ideally named something descriptive related to the functionality you're implementing (the name of your component is a good choice for this).

## Creating the new component file

The standard naming scheme for components is as follows:

You need to create a new file called `<YourComponentName>.tsx` within the `Odyssey/src/herbie` directory. This file will contain all of the logic for the React component you're looking to add. Optionally, you can also create a `<YourComponentName>.css` file for any CSS styling your component might need.

## A basic template

The following is a basic template for a skeleton React component (this will go in your tsx file you just created):

```
Expand Down Expand Up @@ -56,15 +61,16 @@ export { YourComponent };
```

## Identifying the parent component

Your component will almost certainly be a child component of one of the pre-existing components in the codebase. The next step here is to identify which component this is. Some common parent components are:

- `ExpressionTable.tsx`, which is the component to render the table of expressions on the right half of Odyssey, and which contains child components that have interactions with individual expressions, such as the Derivation component (which shows the derivation of a particular expression) or the Local Error component (which shows localized error for the individual parts of an expression)

- Left-side global `SelectableVisualization.tsx`, which is the component to render the various visualizations on the left half of Odyssey, and which contains a drop-down menu from which the user can choose multiple different visualizations.

It is possible for the same component to be reused in multiple locations, and thus have multiple parent components - in that case, it should be added in each location where the component should appear. This also makes it very flexible to modify the location of the component.

## Adding a component to the ExpressionTable

If your component has the ExpressionTable as a parent component, navigate to `ExpressionTable.tsx` and find the HTML element `<div className="expressions-actual">` under the returned HTML element. Under this component, you should see something like the following (as of October 2024):

```
Expand All @@ -83,6 +89,7 @@ which contains all of the components rendered as part of the Expression Table's
**Importantly**, note that the ExpressionTable will then pass all of these components into a SelectableVisualization component (not to be confused with the left-side SelectableVisualization) - this component corresponds to the individual dropdowns available for each expression on the **right half** of Odyssey, not the single selectable visualization on the left half of Odyssey.

## Adding a component to the Left-Side SelectableVisualization

If your component has the left-side SelectableVisualization as a parent component, navigate to `HerbieUI.tsx` and find the HTML element <SelectableVisualization>. The element in question should have a `components`, defined in the same file as something like (as of October 2024):

```
Expand All @@ -93,9 +100,10 @@ const components = [
];
```

which contains all of the components rendered as part of the Selectable Visualization. Add an import for your component at the top of the `HerbieUI.tsx` file, and then you should be able to add your component here and have it rendered as an option in the Selectable Visualization.
which contains all of the components rendered as part of the Selectable Visualization. Add an import for your component at the top of the `HerbieUI.tsx` file, and then you should be able to add your component here and have it rendered as an option in the Selectable Visualization.

## Global state and contexts

You may find that your component requires access to some of the global state shared across all of Odyssey. This state is almost always contained within a [Context](https://react.dev/learn/passing-data-deeply-with-context) from `HerbieContext.ts`.

To use these Contexts, simply add
Expand All @@ -119,6 +127,7 @@ export const YourNewContext = makeGlobal('Your context state here')
```

## Creating custom types

You might also find yourself needing custom types to package data cleanly - these should be defined and set up in `HerbieTypes.ts`.

These work the same way as standard TypeScript [types](https://www.typescriptlang.org/docs/handbook/2/objects.html).
197 changes: 197 additions & 0 deletions doc/adding_external_tool_backend.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
# Adding an external tool server to Odyssey's backend

The purpose of this documentation is to explain how to add a new tool as a binary to Odyssey's backend, and ensure it can be set up when users run Odyssey.

This documentation assumes that you have the repo set up locally, that you have installed all necessary dependencies, and that you can compile, build, and then run Odyssey on your local machine.

The codebase makes heavy use of [React](https://react.dev/), which you should be familiar with before constructing a component. There exist tutorials [for more modern React](https://react.dev/learn) as well as [this older React tutorial](https://legacy.reactjs.org/tutorial/tutorial.html).

TypeScript is the language of choice for this codebase. It features a fairly powerful [type system](https://www.typescriptlang.org/docs/handbook/intro.html) that you should be familiar with.


## Basic Setup

All external tool backend integration will be in ``extension.ts``, found in the top-level directory of the Odyssey repository.

The rest of this tutorial will assume you are looking at the ``extension.ts`` file unless otherwise stated.


## Hosting and linking an address for your binary download

The first step is to identify some location to store your external tool's binary, and include a download address.

Near the very top of ``extension.ts``, you should see a sequence of server address constants:

```
const HERBIE_SERVER_ADDRESS = "https://github.com/herbie-fp/odyssey/releases/download/v1.1.0-bin/herbie-dist.zip"
const FPTAYLOR_SERVER_ADDRESS = "https://github.com/herbie-fp/odyssey/releases/download/fptaylor-component/fptaylor-dist.zip"
const FPBENCH_SERVER_ADDRESS = "https://github.com/herbie-fp/odyssey/releases/download/fptaylor-component/fpbench-dist.zip"
```

that should look like the above as of November 2024.

You should construct a similar constant for your external binary, and provide some link to which your binary can be downloaded.


## Separating different binaries for different distributions
At the beginning of the ``activate`` function,


```
export function activate(context: vscode.ExtensionContext) {
const odysseyDir = require('os').homedir() + '/.local/share/odyssey'
let herbiePath = ''
let fpbenchPath = ''
let fptaylorPath = ''
switch (process.platform) {
case 'win32':
herbiePath = odysseyDir + '/dist/windows/herbie-compiled/herbie.exe'
fpbenchPath = odysseyDir + '/dist/windows/fpbench-compiled/fpbench.exe'
fptaylorPath = odysseyDir + '/dist/windows/fptaylor-compiled/fptaylor.exe'
break
case 'linux':
herbiePath = odysseyDir + '/dist/linux/herbie-compiled/bin/herbie'
fpbenchPath = odysseyDir + '/dist/linux/fpbench-compiled/bin/fpbench'
fptaylorPath = odysseyDir + '/dist/linux/fptaylor-compiled/fptaylor'
break
case 'darwin':
herbiePath = odysseyDir + '/dist/macos/herbie-compiled/bin/herbie'
fpbenchPath = odysseyDir + '/dist/macos/fpbench-compiled/bin/fpbench'
fptaylorPath = odysseyDir + '/dist/macos/fptaylor-compiled/fptaylor'
break
}
// The activate function continues from here...
```

In this portion, define a new path variable for your binary, and add paths for the various platformed binaries your tool has. You should follow the existing structure, which to place the various paths for your tool's binaries under the corresponding OS subfolder under ``dist``, and then under a folder named ``<YOUR_TOOL>-compiled/<YOUR_TOOL>``

Note that ``darwin`` refers to the core operating system behind most distributions of macOS.


## Running the tool with an Express server
Later on in the activate function, you should see the following setup for express:

```
const app = express();
app.use(cors());
app.use(bodyParser.urlencoded({ extended: true }));
app.use(bodyParser.json());
```

You should add an endpoint for your backend tool here. The endpoint will look something like this:

```
app.post('/<YOUR_TOOL>/exec', async (req: any, res: any) => {
const input = req.body;
const safe_input = input.<YOUR_TOOL_INPUT>.replace(/'/g, "\\'");
try {
const { stdout, stderr } = await exec(
`cd ${odysseyDir} && .${<YOUR_TOOL_PATH>.replace(odysseyDir, '')} <(printf '${safe_input}')`,
{ shell: '/bin/bash' }
);
res.json({ stdout: `<(printf "${stdout}")` });
} catch (e) {
console.error(e);
}
})
```

The existing FPTaylor and FPBench endpoints serve as good examples to guide your implementation.


## Writing a function to download and install your tool
Next, we need to write a function to download, unpack, and then install your tool, or otherwise setup the binary.

This will look something like the following:


```
const download_YOUR_TOOL = async () => {
// show information message
vscode.window.showInformationMessage('Downloading <YOUR_TOOL>...')
// spawn the download process
// get zip file from site
const url = <YOUR_TOOL_SERVER_ADDRESS>
// download with curl to home local share odyssey
const home = require('os').homedir()
// TODO path.join instead of string concat
const odysseyDir = home + '/.local/share/odyssey'
if (!fs.existsSync(odysseyDir)) {
fs.mkdirSync(odysseyDir, { recursive: true })
}
if (!fs.existsSync(odysseyDir + '/bin')) {
fs.mkdirSync(odysseyDir + '/bin')
}
if (!fs.existsSync(odysseyDir + '/dist')) {
fs.mkdirSync(odysseyDir + '/dist')
}
const dest = home + '/.local/share/odyssey/<YOUR_TOOL>-compiled.zip'
downloadFile(url, dest, (err: any) => {
if (err) {
vscode.window.showErrorMessage('Error downloading FPTaylor: ' + err, 'Copy to clipboard').then((action) => {
if (action === 'Copy to clipboard') {
vscode.env.clipboard.writeText(err)
}
})
} else {
vscode.window.showInformationMessage('<YOUR_TOOL> downloaded successfully. Please wait while it is installed...')
}
// unzip to home local share odyssey
const AdmZip = require("adm-zip");
try {
const zip = new AdmZip(dest);
zip.extractAllTo(/*target path*/ odysseyDir + '/dist', /*overwrite*/ true);
} catch (e) {
vscode.window.showErrorMessage('Error installing <YOUR_TOOL> (extraction): ' + err, 'Copy to clipboard').then((action) => {
if (action === 'Copy to clipboard') {
vscode.env.clipboard.writeText(err)
}
})
}
try {
fs.unlinkSync(dest)
// make binary executable
fs.chmodSync(<YOUR_TOOL_PATH>, 0o755)
} catch (err: any) {
vscode.window.showErrorMessage('Error installing <YOUR_TOOL>: ' + err, 'Copy to clipboard').then((action) => {
if (action === 'Copy to clipboard') {
vscode.env.clipboard.writeText(err)
}
})
}
})
}
```

As with the previous section, the existing FPTaylor and FPBench endpoints serve as good examples to guide your implementation.


## Prompting the user to download and install your tool
Next, we need to link a button prompt to the function from the previous part.

Following the running of the openTab command itself, you should see the line of code
```
let disposable = vscode.commands.registerCommand(`${extensionName}.openTab`, async () => {
```

with a section for prompting the user to download specific tools if they don't have them already.

Add an error prompt for your tool with a call to the function from the previous part. It should generally look something like the following.

```
if (!fs.existsSync(<YOUR_TOOL_PATH>)) {
// wait for user to download <YOUR_TOOL>
vscode.window.showErrorMessage("<YOUR_TOOL> doesn't seem to be installed yet. Click the button to download it.", 'Download').then((action) => {
if (action === 'Download') {
download_YOUR_TOOL()
}
})
}
```

As always, the existing FPTaylor and FPBench endpoints serve as good examples to guide your implementation.

0 comments on commit 7ef7154

Please sign in to comment.