RustBook/ch14-03-cargo-workspaces.html

495 lines
39 KiB
HTML
Raw Normal View History

2020-01-06 20:57:15 +00:00
<!DOCTYPE HTML>
<html lang="en" class="sidebar-visible no-js light">
<head>
<!-- Book generated using mdBook -->
<meta charset="UTF-8">
<title>Cargo Workspaces - The Rust Programming Language</title>
<meta content="text/html; charset=utf-8" http-equiv="Content-Type">
<meta name="description" content="">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="theme-color" content="#ffffff" />
<link rel="shortcut icon" href="favicon.png">
<link rel="stylesheet" href="css/variables.css">
<link rel="stylesheet" href="css/general.css">
<link rel="stylesheet" href="css/chrome.css">
<link rel="stylesheet" href="css/print.css" media="print">
<!-- Fonts -->
<link rel="stylesheet" href="FontAwesome/css/font-awesome.css">
<link href="googleFonts/css.css" rel="stylesheet" type="text/css">
<!-- Highlight.js Stylesheets -->
<link rel="stylesheet" href="highlight.css">
<link rel="stylesheet" href="tomorrow-night.css">
<link rel="stylesheet" href="ayu-highlight.css">
<!-- Custom theme stylesheets -->
<link rel="stylesheet" href="ferris.css">
<link rel="stylesheet" href="theme/2018-edition.css">
</head>
<body>
<!-- Provide site root to javascript -->
<script type="text/javascript">
var path_to_root = "";
var default_theme = window.matchMedia("(prefers-color-scheme: dark)").matches ? "light" : "light";
</script>
<!-- Work around some values being stored in localStorage wrapped in quotes -->
<script type="text/javascript">
try {
var theme = localStorage.getItem('mdbook-theme');
var sidebar = localStorage.getItem('mdbook-sidebar');
if (theme.startsWith('"') && theme.endsWith('"')) {
localStorage.setItem('mdbook-theme', theme.slice(1, theme.length - 1));
}
if (sidebar.startsWith('"') && sidebar.endsWith('"')) {
localStorage.setItem('mdbook-sidebar', sidebar.slice(1, sidebar.length - 1));
}
} catch (e) { }
</script>
<!-- Set the theme before any content is loaded, prevents flash -->
<script type="text/javascript">
var theme;
try { theme = localStorage.getItem('mdbook-theme'); } catch(e) { }
if (theme === null || theme === undefined) { theme = default_theme; }
var html = document.querySelector('html');
html.classList.remove('no-js')
html.classList.remove('light')
html.classList.add(theme);
html.classList.add('js');
</script>
<!-- Hide / unhide sidebar before it is displayed -->
<script type="text/javascript">
var html = document.querySelector('html');
var sidebar = 'hidden';
if (document.body.clientWidth >= 1080) {
try { sidebar = localStorage.getItem('mdbook-sidebar'); } catch(e) { }
sidebar = sidebar || 'visible';
}
html.classList.remove('sidebar-visible');
html.classList.add("sidebar-" + sidebar);
</script>
<nav id="sidebar" class="sidebar" aria-label="Table of contents">
<div id="sidebar-scrollbox" class="sidebar-scrollbox">
<ol class="chapter"><li class="expanded affix "><a href="title-page.html">The Rust Programming Language</a></li><li class="expanded affix "><a href="foreword.html">Foreword</a></li><li class="expanded affix "><a href="ch00-00-introduction.html">Introduction</a></li><li class="expanded "><a href="ch01-00-getting-started.html"><strong aria-hidden="true">1.</strong> Getting Started</a></li><li><ol class="section"><li class="expanded "><a href="ch01-01-installation.html"><strong aria-hidden="true">1.1.</strong> Installation</a></li><li class="expanded "><a href="ch01-02-hello-world.html"><strong aria-hidden="true">1.2.</strong> Hello, World!</a></li><li class="expanded "><a href="ch01-03-hello-cargo.html"><strong aria-hidden="true">1.3.</strong> Hello, Cargo!</a></li></ol></li><li class="expanded "><a href="ch02-00-guessing-game-tutorial.html"><strong aria-hidden="true">2.</strong> Programming a Guessing Game</a></li><li class="expanded "><a href="ch03-00-common-programming-concepts.html"><strong aria-hidden="true">3.</strong> Common Programming Concepts</a></li><li><ol class="section"><li class="expanded "><a href="ch03-01-variables-and-mutability.html"><strong aria-hidden="true">3.1.</strong> Variables and Mutability</a></li><li class="expanded "><a href="ch03-02-data-types.html"><strong aria-hidden="true">3.2.</strong> Data Types</a></li><li class="expanded "><a href="ch03-03-how-functions-work.html"><strong aria-hidden="true">3.3.</strong> Functions</a></li><li class="expanded "><a href="ch03-04-comments.html"><strong aria-hidden="true">3.4.</strong> Comments</a></li><li class="expanded "><a href="ch03-05-control-flow.html"><strong aria-hidden="true">3.5.</strong> Control Flow</a></li></ol></li><li class="expanded "><a href="ch04-00-understanding-ownership.html"><strong aria-hidden="true">4.</strong> Understanding Ownership</a></li><li><ol class="section"><li class="expanded "><a href="ch04-01-what-is-ownership.html"><strong aria-hidden="true">4.1.</strong> What is Ownership?</a></li><li class="expanded "><a href="ch04-02-references-and-borrowing.html"><strong aria-hidden="true">4.2.</strong> References and Borrowing</a></li><li class="expanded "><a href="ch04-03-slices.html"><strong aria-hidden="true">4.3.</strong> The Slice Type</a></li></ol></li><li class="expanded "><a href="ch05-00-structs.html"><strong aria-hidden="true">5.</strong> Using Structs to Structure Related Data</a></li><li><ol class="section"><li class="expanded "><a href="ch05-01-defining-structs.html"><strong aria-hidden="true">5.1.</strong> Defining and Instantiating Structs</a></li><li class="expanded "><a href="ch05-02-example-structs.html"><strong aria-hidden="true">5.2.</strong> An Example Program Using Structs</a></li><li class="expanded "><a href="ch05-03-method-syntax.html"><strong aria-hidden="true">5.3.</strong> Method Syntax</a></li></ol></li><li class="expanded "><a href="ch06-00-enums.html"><strong aria-hidden="true">6.</strong> Enums and Pattern Matching</a></li><li><ol class="section"><li class="expanded "><a href="ch06-01-defining-an-enum.html"><strong aria-hidden="true">6.1.</strong> Defining an Enum</a></li><li class="expanded "><a href="ch06-02-match.html"><strong aria-hidden="true">6.2.</strong> The match Control Flow Operator</a></li><li class="expanded "><a href="ch06-03-if-let.html"><strong aria-hidden="true">6.3.</strong> Concise Control Flow with if let</a></li></ol></li><li class="expanded "><a href="ch07-00-managing-growing-projects-with-packages-crates-and-modules.html"><strong aria-hidden="true">7.</strong> Managing Growing Projects with Packages, Crates, and Modules</a></li><li><ol class="section"><li class="expanded "><a href="ch07-01-packages-and-crates.html"><strong aria-hidden="true">7.1.</strong> Packages and Crates</a></li><li class="expanded "><a href="ch07-02-defining-modules-to-control-scope-and-privacy.html"><strong aria-hidden="true">7.2.</strong> Defining Modules to Control Scope and Privacy</a></li><li class="expanded "><a href="ch07-03-paths-for-referring-to-an-item-in-the-module-tree.html"><
</div>
<div id="sidebar-resize-handle" class="sidebar-resize-handle"></div>
</nav>
<div id="page-wrapper" class="page-wrapper">
<div class="page">
<div id="menu-bar" class="menu-bar">
<div id="menu-bar-sticky-container">
<div class="left-buttons">
<button id="sidebar-toggle" class="icon-button" type="button" title="Toggle Table of Contents" aria-label="Toggle Table of Contents" aria-controls="sidebar">
<i class="fa fa-bars"></i>
</button>
<button id="theme-toggle" class="icon-button" type="button" title="Change theme" aria-label="Change theme" aria-haspopup="true" aria-expanded="false" aria-controls="theme-list">
<i class="fa fa-paint-brush"></i>
</button>
<ul id="theme-list" class="theme-popup" aria-label="Themes" role="menu">
<li role="none"><button role="menuitem" class="theme" id="light">Light (default)</button></li>
<li role="none"><button role="menuitem" class="theme" id="rust">Rust</button></li>
<li role="none"><button role="menuitem" class="theme" id="coal">Coal</button></li>
<li role="none"><button role="menuitem" class="theme" id="navy">Navy</button></li>
<li role="none"><button role="menuitem" class="theme" id="ayu">Ayu</button></li>
</ul>
<button id="search-toggle" class="icon-button" type="button" title="Search. (Shortkey: s)" aria-label="Toggle Searchbar" aria-expanded="false" aria-keyshortcuts="S" aria-controls="searchbar">
<i class="fa fa-search"></i>
</button>
</div>
<h1 class="menu-title">The Rust Programming Language</h1>
<div class="right-buttons">
<a href="print.html" title="Print this book" aria-label="Print this book">
<i id="print-button" class="fa fa-print"></i>
</a>
</div>
</div>
</div>
<div id="search-wrapper" class="hidden">
<form id="searchbar-outer" class="searchbar-outer">
<input type="search" name="search" id="searchbar" name="searchbar" placeholder="Search this book ..." aria-controls="searchresults-outer" aria-describedby="searchresults-header">
</form>
<div id="searchresults-outer" class="searchresults-outer hidden">
<div id="searchresults-header" class="searchresults-header"></div>
<ul id="searchresults">
</ul>
</div>
</div>
<!-- Apply ARIA attributes after the sidebar and the sidebar toggle button are added to the DOM -->
<script type="text/javascript">
document.getElementById('sidebar-toggle').setAttribute('aria-expanded', sidebar === 'visible');
document.getElementById('sidebar').setAttribute('aria-hidden', sidebar !== 'visible');
Array.from(document.querySelectorAll('#sidebar a')).forEach(function(link) {
link.setAttribute('tabIndex', sidebar === 'visible' ? 0 : -1);
});
</script>
<div id="content" class="content">
<main>
<h2><a class="header" href="#cargo-workspaces" id="cargo-workspaces">Cargo Workspaces</a></h2>
<p>In Chapter 12, we built a package that included a binary crate and a library
crate. As your project develops, you might find that the library crate
continues to get bigger and you want to split up your package further into
multiple library crates. In this situation, Cargo offers a feature called
<em>workspaces</em> that can help manage multiple related packages that are developed
in tandem.</p>
<h3><a class="header" href="#creating-a-workspace" id="creating-a-workspace">Creating a Workspace</a></h3>
<p>A <em>workspace</em> is a set of packages that share the same <em>Cargo.lock</em> and output
directory. Lets make a project using a workspace—well use trivial code so we
can concentrate on the structure of the workspace. There are multiple ways to
structure a workspace; were going to show one common way. Well have a
workspace containing a binary and two libraries. The binary, which will provide
the main functionality, will depend on the two libraries. One library will
provide an <code>add_one</code> function, and a second library an <code>add_two</code> function.
These three crates will be part of the same workspace. Well start by creating
a new directory for the workspace:</p>
<pre><code class="language-text">$ mkdir add
$ cd add
</code></pre>
<p>Next, in the <em>add</em> directory, we create the <em>Cargo.toml</em> file that will
configure the entire workspace. This file wont have a <code>[package]</code> section or
the metadata weve seen in other <em>Cargo.toml</em> files. Instead, it will start
with a <code>[workspace]</code> section that will allow us to add members to the workspace
by specifying the path to our binary crate; in this case, that path is <em>adder</em>:</p>
<p><span class="filename">Filename: Cargo.toml</span></p>
<pre><code class="language-toml">[workspace]
members = [
&quot;adder&quot;,
]
</code></pre>
<p>Next, well create the <code>adder</code> binary crate by running <code>cargo new</code> within the
<em>add</em> directory:</p>
<pre><code class="language-text">$ cargo new adder
Created binary (application) `adder` project
</code></pre>
<p>At this point, we can build the workspace by running <code>cargo build</code>. The files
in your <em>add</em> directory should look like this:</p>
<pre><code class="language-text">├── Cargo.lock
├── Cargo.toml
├── adder
│ ├── Cargo.toml
│ └── src
│ └── main.rs
└── target
</code></pre>
<p>The workspace has one <em>target</em> directory at the top level for the compiled
artifacts to be placed into; the <code>adder</code> crate doesnt have its own <em>target</em>
directory. Even if we were to run <code>cargo build</code> from inside the <em>adder</em>
directory, the compiled artifacts would still end up in <em>add/target</em> rather
than <em>add/adder/target</em>. Cargo structures the <em>target</em> directory in a workspace
like this because the crates in a workspace are meant to depend on each other.
If each crate had its own <em>target</em> directory, each crate would have to
recompile each of the other crates in the workspace to have the artifacts in
its own <em>target</em> directory. By sharing one <em>target</em> directory, the crates can
avoid unnecessary rebuilding.</p>
<h3><a class="header" href="#creating-the-second-crate-in-the-workspace" id="creating-the-second-crate-in-the-workspace">Creating the Second Crate in the Workspace</a></h3>
<p>Next, lets create another member crate in the workspace and call it <code>add-one</code>.
Change the top-level <em>Cargo.toml</em> to specify the <em>add-one</em> path in the
<code>members</code> list:</p>
<p><span class="filename">Filename: Cargo.toml</span></p>
<pre><code class="language-toml">[workspace]
members = [
&quot;adder&quot;,
&quot;add-one&quot;,
]
</code></pre>
<p>Then generate a new library crate named <code>add-one</code>:</p>
<pre><code class="language-text">$ cargo new add-one --lib
Created library `add-one` project
</code></pre>
<p>Your <em>add</em> directory should now have these directories and files:</p>
<pre><code class="language-text">├── Cargo.lock
├── Cargo.toml
├── add-one
│ ├── Cargo.toml
│ └── src
│ └── lib.rs
├── adder
│ ├── Cargo.toml
│ └── src
│ └── main.rs
└── target
</code></pre>
<p>In the <em>add-one/src/lib.rs</em> file, lets add an <code>add_one</code> function:</p>
<p><span class="filename">Filename: add-one/src/lib.rs</span></p>
<pre><pre class="playpen"><code class="language-rust">
<span class="boring">#![allow(unused_variables)]
</span><span class="boring">fn main() {
</span>pub fn add_one(x: i32) -&gt; i32 {
x + 1
}
<span class="boring">}
</span></code></pre></pre>
<p>Now that we have a library crate in the workspace, we can have the binary crate
<code>adder</code> depend on the library crate <code>add-one</code>. First, well need to add a path
dependency on <code>add-one</code> to <em>adder/Cargo.toml</em>.</p>
<p><span class="filename">Filename: adder/Cargo.toml</span></p>
<pre><code class="language-toml">[dependencies]
add-one = { path = &quot;../add-one&quot; }
</code></pre>
<p>Cargo doesnt assume that crates in a workspace will depend on each other, so
we need to be explicit about the dependency relationships between the crates.</p>
<p>Next, lets use the <code>add_one</code> function from the <code>add-one</code> crate in the <code>adder</code>
crate. Open the <em>adder/src/main.rs</em> file and add a <code>use</code> line at the top to
bring the new <code>add-one</code> library crate into scope. Then change the <code>main</code>
function to call the <code>add_one</code> function, as in Listing 14-7.</p>
<p><span class="filename">Filename: adder/src/main.rs</span></p>
<pre><code class="language-rust ignore">use add_one;
fn main() {
let num = 10;
println!(&quot;Hello, world! {} plus one is {}!&quot;, num, add_one::add_one(num));
}
</code></pre>
<p><span class="caption">Listing 14-7: Using the <code>add-one</code> library crate from the
<code>adder</code> crate</span></p>
<p>Lets build the workspace by running <code>cargo build</code> in the top-level <em>add</em>
directory!</p>
<pre><code class="language-text">$ cargo build
Compiling add-one v0.1.0 (file:///projects/add/add-one)
Compiling adder v0.1.0 (file:///projects/add/adder)
Finished dev [unoptimized + debuginfo] target(s) in 0.68 secs
</code></pre>
<p>To run the binary crate from the <em>add</em> directory, we need to specify which
package in the workspace we want to use by using the <code>-p</code> argument and the
package name with <code>cargo run</code>:</p>
<pre><code class="language-text">$ cargo run -p adder
Finished dev [unoptimized + debuginfo] target(s) in 0.0 secs
Running `target/debug/adder`
Hello, world! 10 plus one is 11!
</code></pre>
<p>This runs the code in <em>adder/src/main.rs</em>, which depends on the <code>add-one</code> crate.</p>
<h4><a class="header" href="#depending-on-an-external-crate-in-a-workspace" id="depending-on-an-external-crate-in-a-workspace">Depending on an External Crate in a Workspace</a></h4>
<p>Notice that the workspace has only one <em>Cargo.lock</em> file at the top level of
the workspace rather than having a <em>Cargo.lock</em> in each crates directory. This
ensures that all crates are using the same version of all dependencies. If we
add the <code>rand</code> crate to the <em>adder/Cargo.toml</em> and <em>add-one/Cargo.toml</em>
files, Cargo will resolve both of those to one version of <code>rand</code> and record
that in the one <em>Cargo.lock</em>. Making all crates in the workspace use the same
dependencies means the crates in the workspace will always be compatible with
each other. Lets add the <code>rand</code> crate to the <code>[dependencies]</code> section in the
<em>add-one/Cargo.toml</em> file to be able to use the <code>rand</code> crate in the <code>add-one</code>
crate:</p>
<!-- When updating the version of `rand` used, also update the version of
`rand` used in these files so they all match:
* ch02-00-guessing-game-tutorial.md
* ch07-04-bringing-paths-into-scope-with-the-use-keyword.md
-->
<p><span class="filename">Filename: add-one/Cargo.toml</span></p>
<pre><code class="language-toml">[dependencies]
rand = &quot;0.5.5&quot;
</code></pre>
<p>We can now add <code>use rand;</code> to the <em>add-one/src/lib.rs</em> file, and building the
whole workspace by running <code>cargo build</code> in the <em>add</em> directory will bring in
and compile the <code>rand</code> crate:</p>
<pre><code class="language-text">$ cargo build
Updating crates.io index
Downloaded rand v0.5.5
--snip--
Compiling rand v0.5.5
Compiling add-one v0.1.0 (file:///projects/add/add-one)
Compiling adder v0.1.0 (file:///projects/add/adder)
Finished dev [unoptimized + debuginfo] target(s) in 10.18 secs
</code></pre>
<p>The top-level <em>Cargo.lock</em> now contains information about the dependency of
<code>add-one</code> on <code>rand</code>. However, even though <code>rand</code> is used somewhere in the
workspace, we cant use it in other crates in the workspace unless we add
<code>rand</code> to their <em>Cargo.toml</em> files as well. For example, if we add <code>use rand;</code>
to the <em>adder/src/main.rs</em> file for the <code>adder</code> crate, well get an error:</p>
<pre><code class="language-text">$ cargo build
Compiling adder v0.1.0 (file:///projects/add/adder)
error: use of unstable library feature 'rand': use `rand` from crates.io (see
issue #27703)
--&gt; adder/src/main.rs:1:1
|
1 | use rand;
</code></pre>
<p>To fix this, edit the <em>Cargo.toml</em> file for the <code>adder</code> crate and indicate that
<code>rand</code> is a dependency for that crate as well. Building the <code>adder</code> crate will
add <code>rand</code> to the list of dependencies for <code>adder</code> in <em>Cargo.lock</em>, but no
additional copies of <code>rand</code> will be downloaded. Cargo has ensured that every
crate in the workspace using the <code>rand</code> crate will be using the same version.
Using the same version of <code>rand</code> across the workspace saves space because we
wont have multiple copies and ensures that the crates in the workspace will be
compatible with each other.</p>
<h4><a class="header" href="#adding-a-test-to-a-workspace" id="adding-a-test-to-a-workspace">Adding a Test to a Workspace</a></h4>
<p>For another enhancement, lets add a test of the <code>add_one::add_one</code> function
within the <code>add_one</code> crate:</p>
<p><span class="filename">Filename: add-one/src/lib.rs</span></p>
<pre><pre class="playpen"><code class="language-rust">
<span class="boring">#![allow(unused_variables)]
</span><span class="boring">fn main() {
</span>pub fn add_one(x: i32) -&gt; i32 {
x + 1
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn it_works() {
assert_eq!(3, add_one(2));
}
}
<span class="boring">}
</span></code></pre></pre>
<p>Now run <code>cargo test</code> in the top-level <em>add</em> directory:</p>
<pre><code class="language-text">$ cargo test
Compiling add-one v0.1.0 (file:///projects/add/add-one)
Compiling adder v0.1.0 (file:///projects/add/adder)
Finished dev [unoptimized + debuginfo] target(s) in 0.27 secs
Running target/debug/deps/add_one-f0253159197f7841
running 1 test
test tests::it_works ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
Running target/debug/deps/adder-f88af9d2cc175a5e
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
Doc-tests add-one
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
</code></pre>
<p>The first section of the output shows that the <code>it_works</code> test in the <code>add-one</code>
crate passed. The next section shows that zero tests were found in the <code>adder</code>
crate, and then the last section shows zero documentation tests were found in
the <code>add-one</code> crate. Running <code>cargo test</code> in a workspace structured like this
one will run the tests for all the crates in the workspace.</p>
<p>We can also run tests for one particular crate in a workspace from the
top-level directory by using the <code>-p</code> flag and specifying the name of the crate
we want to test:</p>
<pre><code class="language-text">$ cargo test -p add-one
Finished dev [unoptimized + debuginfo] target(s) in 0.0 secs
Running target/debug/deps/add_one-b3235fea9a156f74
running 1 test
test tests::it_works ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
Doc-tests add-one
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
</code></pre>
<p>This output shows <code>cargo test</code> only ran the tests for the <code>add-one</code> crate and
didnt run the <code>adder</code> crate tests.</p>
<p>If you publish the crates in the workspace to <a href="https://crates.io/">crates.io</a>,
each crate in the workspace will need to be published separately. The <code>cargo publish</code> command does not have an <code>--all</code> flag or a <code>-p</code> flag, so you must
change to each crates directory and run <code>cargo publish</code> on each crate in the
workspace to publish the crates.</p>
<p>For additional practice, add an <code>add-two</code> crate to this workspace in a similar
way as the <code>add-one</code> crate!</p>
<p>As your project grows, consider using a workspace: its easier to understand
smaller, individual components than one big blob of code. Furthermore, keeping
the crates in a workspace can make coordination between them easier if they are
often changed at the same time.</p>
</main>
<nav class="nav-wrapper" aria-label="Page navigation">
<!-- Mobile navigation buttons -->
<a rel="prev" href="ch14-02-publishing-to-crates-io.html" class="mobile-nav-chapters previous" title="Previous chapter" aria-label="Previous chapter" aria-keyshortcuts="Left">
<i class="fa fa-angle-left"></i>
</a>
<a rel="next" href="ch14-04-installing-binaries.html" class="mobile-nav-chapters next" title="Next chapter" aria-label="Next chapter" aria-keyshortcuts="Right">
<i class="fa fa-angle-right"></i>
</a>
<div style="clear: both"></div>
</nav>
</div>
</div>
<nav class="nav-wide-wrapper" aria-label="Page navigation">
<a href="ch14-02-publishing-to-crates-io.html" class="nav-chapters previous" title="Previous chapter" aria-label="Previous chapter" aria-keyshortcuts="Left">
<i class="fa fa-angle-left"></i>
</a>
<a href="ch14-04-installing-binaries.html" class="nav-chapters next" title="Next chapter" aria-label="Next chapter" aria-keyshortcuts="Right">
<i class="fa fa-angle-right"></i>
</a>
</nav>
</div>
<script type="text/javascript">
window.playpen_copyable = true;
</script>
<script src="elasticlunr.min.js" type="text/javascript" charset="utf-8"></script>
<script src="mark.min.js" type="text/javascript" charset="utf-8"></script>
<script src="searcher.js" type="text/javascript" charset="utf-8"></script>
<script src="clipboard.min.js" type="text/javascript" charset="utf-8"></script>
<script src="highlight.js" type="text/javascript" charset="utf-8"></script>
<script src="book.js" type="text/javascript" charset="utf-8"></script>
<!-- Custom JS scripts -->
<script type="text/javascript" src="ferris.js"></script>
</body>
</html>