Skip to content

Commit 848f7b7

Browse files
committed
rustdoc: Implement cross-crate searching
A major discoverability issue with rustdoc is that all crates have their documentation built in isolation, so it's difficult when looking at the documentation for libstd to learn that there's a libcollections crate with a HashMap in it. This commit moves rustdoc a little closer to improving the multiple crate experience. This unifies all search indexes for all crates into one file so all pages share the same search index. This allows searching to work across crates in the same documentation directory (as the standard distribution is currently built). This strategy involves updating a shared file amongst many rustdoc processes, so I implemented a simple file locking API for handling synchronization for updates to the shared files. cc #12554
1 parent d717d61 commit 848f7b7

File tree

6 files changed

+383
-157
lines changed

6 files changed

+383
-157
lines changed

src/librustdoc/flock.rs

Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
// Copyright 2014 The Rust Project Developers. See the COPYRIGHT
2+
// file at the top-level directory of this distribution and at
3+
// http://rust-lang.org/COPYRIGHT.
4+
//
5+
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
6+
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
7+
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
8+
// option. This file may not be copied, modified, or distributed
9+
// except according to those terms.
10+
11+
//! Simple file-locking apis for each OS.
12+
//!
13+
//! This is not meant to be in the standard library, it does nothing with
14+
//! green/native threading. This is just a bare-bones enough solution for
15+
//! librustdoc, it is not production quality at all.
16+
17+
#[allow(non_camel_case_types)];
18+
19+
pub use self::imp::Lock;
20+
21+
#[cfg(unix)]
22+
mod imp {
23+
use std::libc;
24+
25+
#[cfg(target_os = "linux")]
26+
mod os {
27+
use std::libc;
28+
29+
pub struct flock {
30+
l_type: libc::c_short,
31+
l_whence: libc::c_short,
32+
l_start: libc::off_t,
33+
l_len: libc::off_t,
34+
l_pid: libc::pid_t,
35+
36+
// not actually here, but brings in line with freebsd
37+
l_sysid: libc::c_int,
38+
}
39+
40+
pub static F_WRLCK: libc::c_short = 1;
41+
pub static F_UNLCK: libc::c_short = 2;
42+
pub static F_SETLK: libc::c_int = 6;
43+
pub static F_SETLKW: libc::c_int = 7;
44+
}
45+
46+
#[cfg(target_os = "freebsd")]
47+
mod os {
48+
use std::libc;
49+
50+
pub struct flock {
51+
l_start: libc::off_t,
52+
l_len: libc::off_t,
53+
l_pid: libc::pid_t,
54+
l_type: libc::c_short,
55+
l_whence: libc::c_short,
56+
l_sysid: libc::c_int,
57+
}
58+
59+
pub static F_UNLCK: libc::c_short = 2;
60+
pub static F_WRLCK: libc::c_short = 3;
61+
pub static F_SETLK: libc::c_int = 12;
62+
pub static F_SETLKW: libc::c_int = 13;
63+
}
64+
65+
#[cfg(target_os = "macos")]
66+
mod os {
67+
use std::libc;
68+
69+
pub struct flock {
70+
l_start: libc::off_t,
71+
l_len: libc::off_t,
72+
l_pid: libc::pid_t,
73+
l_type: libc::c_short,
74+
l_whence: libc::c_short,
75+
76+
// not actually here, but brings in line with freebsd
77+
l_sysid: libc::c_int,
78+
}
79+
80+
pub static F_UNLCK: libc::c_short = 2;
81+
pub static F_WRLCK: libc::c_short = 3;
82+
pub static F_SETLK: libc::c_int = 8;
83+
pub static F_SETLKW: libc::c_int = 9;
84+
}
85+
86+
pub struct Lock {
87+
priv fd: libc::c_int,
88+
}
89+
90+
impl Lock {
91+
pub fn new(p: &Path) -> Lock {
92+
let fd = p.with_c_str(|s| unsafe {
93+
libc::open(s, libc::O_RDWR | libc::O_CREAT, libc::S_IRWXU)
94+
});
95+
assert!(fd > 0);
96+
let flock = os::flock {
97+
l_start: 0,
98+
l_len: 0,
99+
l_pid: 0,
100+
l_whence: libc::SEEK_SET as libc::c_short,
101+
l_type: os::F_WRLCK,
102+
l_sysid: 0,
103+
};
104+
let ret = unsafe {
105+
libc::fcntl(fd, os::F_SETLKW, &flock as *os::flock)
106+
};
107+
if ret == -1 {
108+
unsafe { libc::close(fd); }
109+
fail!("could not lock `{}`", p.display())
110+
}
111+
Lock { fd: fd }
112+
}
113+
}
114+
115+
impl Drop for Lock {
116+
fn drop(&mut self) {
117+
let flock = os::flock {
118+
l_start: 0,
119+
l_len: 0,
120+
l_pid: 0,
121+
l_whence: libc::SEEK_SET as libc::c_short,
122+
l_type: os::F_UNLCK,
123+
l_sysid: 0,
124+
};
125+
unsafe {
126+
libc::fcntl(self.fd, os::F_SETLK, &flock as *os::flock);
127+
libc::close(self.fd);
128+
}
129+
}
130+
}
131+
}
132+
133+
#[cfg(windows)]
134+
mod imp {
135+
use std::libc;
136+
use std::mem;
137+
use std::os::win32::as_utf16_p;
138+
use std::ptr;
139+
140+
static LOCKFILE_EXCLUSIVE_LOCK: libc::DWORD = 0x00000002;
141+
142+
extern "system" {
143+
fn LockFileEx(hFile: libc::HANDLE,
144+
dwFlags: libc::DWORD,
145+
dwReserved: libc::DWORD,
146+
nNumberOfBytesToLockLow: libc::DWORD,
147+
nNumberOfBytesToLockHigh: libc::DWORD,
148+
lpOverlapped: libc::LPOVERLAPPED) -> libc::BOOL;
149+
fn UnlockFileEx(hFile: libc::HANDLE,
150+
dwReserved: libc::DWORD,
151+
nNumberOfBytesToLockLow: libc::DWORD,
152+
nNumberOfBytesToLockHigh: libc::DWORD,
153+
lpOverlapped: libc::LPOVERLAPPED) -> libc::BOOL;
154+
}
155+
156+
pub struct Lock {
157+
priv handle: libc::HANDLE,
158+
}
159+
160+
impl Lock {
161+
pub fn new(p: &Path) -> Lock {
162+
let handle = as_utf16_p(p.as_str().unwrap(), |p| unsafe {
163+
libc::CreateFileW(p, libc::GENERIC_READ, 0, ptr::mut_null(),
164+
libc::CREATE_ALWAYS,
165+
libc::FILE_ATTRIBUTE_NORMAL,
166+
ptr::mut_null())
167+
});
168+
assert!(handle as uint != libc::INVALID_HANDLE_VALUE as uint);
169+
let mut overlapped: libc::OVERLAPPED = unsafe { mem::init() };
170+
let ret = unsafe {
171+
LockFileEx(handle, LOCKFILE_EXCLUSIVE_LOCK, 0, 100, 0,
172+
&mut overlapped)
173+
};
174+
if ret == 0 {
175+
unsafe { libc::CloseHandle(handle); }
176+
fail!("could not lock `{}`", p.display())
177+
}
178+
Lock { handle: handle }
179+
}
180+
}
181+
182+
impl Drop for Lock {
183+
fn drop(&mut self) {
184+
let mut overlapped: libc::OVERLAPPED = unsafe { mem::init() };
185+
unsafe {
186+
UnlockFileEx(self.handle, 0, 100, 0, &mut overlapped);
187+
libc::CloseHandle(self.handle);
188+
}
189+
}
190+
}
191+
}

src/librustdoc/html/layout.rs

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ pub fn render<T: fmt::Show, S: fmt::Show>(
3737
3838
<link href='http://fonts.googleapis.com/css?family=Oswald:700|Inconsolata:400,700'
3939
rel='stylesheet' type='text/css'>
40-
<link rel=\"stylesheet\" type=\"text/css\" href=\"{root_path}{krate}/main.css\">
40+
<link rel=\"stylesheet\" type=\"text/css\" href=\"{root_path}main.css\">
4141
4242
{favicon, select, none{} other{<link rel=\"shortcut icon\" href=\"#\" />}}
4343
</head>
@@ -74,13 +74,6 @@ pub fn render<T: fmt::Show, S: fmt::Show>(
7474
7575
<section class=\"footer\"></section>
7676
77-
<script>
78-
var rootPath = \"{root_path}\";
79-
</script>
80-
<script src=\"{root_path}{krate}/jquery.js\"></script>
81-
<script src=\"{root_path}{krate}/search-index.js\"></script>
82-
<script src=\"{root_path}{krate}/main.js\"></script>
83-
8477
<div id=\"help\" class=\"hidden\">
8578
<div class=\"shortcuts\">
8679
<h1>Keyboard shortcuts</h1>
@@ -111,6 +104,14 @@ pub fn render<T: fmt::Show, S: fmt::Show>(
111104
</p>
112105
</div>
113106
</div>
107+
108+
<script>
109+
var rootPath = \"{root_path}\";
110+
var currentCrate = \"{krate}\";
111+
</script>
112+
<script src=\"{root_path}jquery.js\"></script>
113+
<script src=\"{root_path}main.js\"></script>
114+
<script async src=\"{root_path}search-index.js\"></script>
114115
</body>
115116
</html>
116117
",

src/librustdoc/html/render.rs

Lines changed: 56 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@
3636
use std::fmt;
3737
use std::local_data;
3838
use std::io;
39-
use std::io::{fs, File, BufferedWriter};
39+
use std::io::{fs, File, BufferedWriter, MemWriter, BufferedReader};
4040
use std::str;
4141
use std::vec;
4242
use std::vec_ng::Vec;
@@ -283,48 +283,75 @@ pub fn run(mut krate: clean::Crate, dst: Path) -> io::IoResult<()> {
283283
};
284284
}
285285

286-
// Add all the static files
287-
let mut dst = cx.dst.join(krate.name.as_slice());
288-
try!(mkdir(&dst));
289-
try!(write(dst.join("jquery.js"),
290-
include_str!("static/jquery-2.1.0.min.js")));
291-
try!(write(dst.join("main.js"), include_str!("static/main.js")));
292-
try!(write(dst.join("main.css"), include_str!("static/main.css")));
293-
try!(write(dst.join("normalize.css"),
294-
include_str!("static/normalize.css")));
295-
296286
// Publish the search index
297-
{
298-
dst.push("search-index.js");
299-
let mut w = BufferedWriter::new(File::create(&dst).unwrap());
300-
let w = &mut w as &mut Writer;
301-
try!(write!(w, "var searchIndex = ["));
287+
let index = {
288+
let mut w = MemWriter::new();
289+
try!(write!(&mut w, "searchIndex['{}'] = [", krate.name));
302290
for (i, item) in cache.search_index.iter().enumerate() {
303291
if i > 0 {
304-
try!(write!(w, ","));
292+
try!(write!(&mut w, ","));
305293
}
306-
try!(write!(w, "\\{ty:\"{}\",name:\"{}\",path:\"{}\",desc:{}",
307-
item.ty, item.name, item.path,
308-
item.desc.to_json().to_str()));
294+
try!(write!(&mut w, "\\{ty:\"{}\",name:\"{}\",path:\"{}\",desc:{}",
295+
item.ty, item.name, item.path,
296+
item.desc.to_json().to_str()));
309297
match item.parent {
310298
Some(id) => {
311-
try!(write!(w, ",parent:'{}'", id));
299+
try!(write!(&mut w, ",parent:'{}'", id));
312300
}
313301
None => {}
314302
}
315-
try!(write!(w, "\\}"));
303+
try!(write!(&mut w, "\\}"));
316304
}
317-
try!(write!(w, "];"));
318-
try!(write!(w, "var allPaths = \\{"));
305+
try!(write!(&mut w, "];"));
306+
try!(write!(&mut w, "allPaths['{}'] = \\{", krate.name));
319307
for (i, (&id, &(ref fqp, short))) in cache.paths.iter().enumerate() {
320308
if i > 0 {
321-
try!(write!(w, ","));
309+
try!(write!(&mut w, ","));
322310
}
323-
try!(write!(w, "'{}':\\{type:'{}',name:'{}'\\}",
324-
id, short, *fqp.last().unwrap()));
311+
try!(write!(&mut w, "'{}':\\{type:'{}',name:'{}'\\}",
312+
id, short, *fqp.last().unwrap()));
325313
}
326-
try!(write!(w, "\\};"));
327-
try!(w.flush());
314+
try!(write!(&mut w, "\\};"));
315+
316+
str::from_utf8_owned(w.unwrap()).unwrap()
317+
};
318+
319+
// Write out the shared files. Note that these are shared among all rustdoc
320+
// docs placed in the output directory, so this needs to be a synchronized
321+
// operation with respect to all other rustdocs running around.
322+
{
323+
try!(mkdir(&cx.dst));
324+
let _lock = ::flock::Lock::new(&cx.dst.join(".lock"));
325+
326+
// Add all the static files. These may already exist, but we just
327+
// overwrite them anyway to make sure that they're fresh and up-to-date.
328+
try!(write(cx.dst.join("jquery.js"),
329+
include_str!("static/jquery-2.1.0.min.js")));
330+
try!(write(cx.dst.join("main.js"), include_str!("static/main.js")));
331+
try!(write(cx.dst.join("main.css"), include_str!("static/main.css")));
332+
try!(write(cx.dst.join("normalize.css"),
333+
include_str!("static/normalize.css")));
334+
335+
// Update the search index
336+
let dst = cx.dst.join("search-index.js");
337+
let mut all_indexes = Vec::new();
338+
all_indexes.push(index);
339+
if dst.exists() {
340+
for line in BufferedReader::new(File::open(&dst)).lines() {
341+
let line = try!(line);
342+
if !line.starts_with("searchIndex") { continue }
343+
if line.starts_with(format!("searchIndex['{}']", krate.name)) {
344+
continue
345+
}
346+
all_indexes.push(line);
347+
}
348+
}
349+
let mut w = try!(File::create(&dst));
350+
try!(writeln!(&mut w, r"var searchIndex = \{\}; var allPaths = \{\};"));
351+
for index in all_indexes.iter() {
352+
try!(writeln!(&mut w, "{}", *index));
353+
}
354+
try!(writeln!(&mut w, "initSearch(searchIndex);"));
328355
}
329356

330357
// Render all source files (this may turn into a giant no-op)

0 commit comments

Comments
 (0)