From 848f7b734ec88964879f5a3051b940d48469ce2e Mon Sep 17 00:00:00 2001 From: Alex Crichton Date: Sun, 16 Mar 2014 01:08:56 -0700 Subject: [PATCH] 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 --- src/librustdoc/flock.rs | 191 +++++++++++++++++++++++ src/librustdoc/html/layout.rs | 17 +- src/librustdoc/html/render.rs | 85 ++++++---- src/librustdoc/html/static/main.js | 242 +++++++++++++++-------------- src/librustdoc/lib.rs | 1 + src/libstd/libc.rs | 2 +- 6 files changed, 382 insertions(+), 156 deletions(-) create mode 100644 src/librustdoc/flock.rs diff --git a/src/librustdoc/flock.rs b/src/librustdoc/flock.rs new file mode 100644 index 00000000000..f5f75575113 --- /dev/null +++ b/src/librustdoc/flock.rs @@ -0,0 +1,191 @@ +// Copyright 2014 The Rust Project Developers. See the COPYRIGHT +// file at the top-level directory of this distribution and at +// http://rust-lang.org/COPYRIGHT. +// +// Licensed under the Apache License, Version 2.0 or the MIT license +// , at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +//! Simple file-locking apis for each OS. +//! +//! This is not meant to be in the standard library, it does nothing with +//! green/native threading. This is just a bare-bones enough solution for +//! librustdoc, it is not production quality at all. + +#[allow(non_camel_case_types)]; + +pub use self::imp::Lock; + +#[cfg(unix)] +mod imp { + use std::libc; + + #[cfg(target_os = "linux")] + mod os { + use std::libc; + + pub struct flock { + l_type: libc::c_short, + l_whence: libc::c_short, + l_start: libc::off_t, + l_len: libc::off_t, + l_pid: libc::pid_t, + + // not actually here, but brings in line with freebsd + l_sysid: libc::c_int, + } + + pub static F_WRLCK: libc::c_short = 1; + pub static F_UNLCK: libc::c_short = 2; + pub static F_SETLK: libc::c_int = 6; + pub static F_SETLKW: libc::c_int = 7; + } + + #[cfg(target_os = "freebsd")] + mod os { + use std::libc; + + pub struct flock { + l_start: libc::off_t, + l_len: libc::off_t, + l_pid: libc::pid_t, + l_type: libc::c_short, + l_whence: libc::c_short, + l_sysid: libc::c_int, + } + + pub static F_UNLCK: libc::c_short = 2; + pub static F_WRLCK: libc::c_short = 3; + pub static F_SETLK: libc::c_int = 12; + pub static F_SETLKW: libc::c_int = 13; + } + + #[cfg(target_os = "macos")] + mod os { + use std::libc; + + pub struct flock { + l_start: libc::off_t, + l_len: libc::off_t, + l_pid: libc::pid_t, + l_type: libc::c_short, + l_whence: libc::c_short, + + // not actually here, but brings in line with freebsd + l_sysid: libc::c_int, + } + + pub static F_UNLCK: libc::c_short = 2; + pub static F_WRLCK: libc::c_short = 3; + pub static F_SETLK: libc::c_int = 8; + pub static F_SETLKW: libc::c_int = 9; + } + + pub struct Lock { + priv fd: libc::c_int, + } + + impl Lock { + pub fn new(p: &Path) -> Lock { + let fd = p.with_c_str(|s| unsafe { + libc::open(s, libc::O_RDWR | libc::O_CREAT, libc::S_IRWXU) + }); + assert!(fd > 0); + let flock = os::flock { + l_start: 0, + l_len: 0, + l_pid: 0, + l_whence: libc::SEEK_SET as libc::c_short, + l_type: os::F_WRLCK, + l_sysid: 0, + }; + let ret = unsafe { + libc::fcntl(fd, os::F_SETLKW, &flock as *os::flock) + }; + if ret == -1 { + unsafe { libc::close(fd); } + fail!("could not lock `{}`", p.display()) + } + Lock { fd: fd } + } + } + + impl Drop for Lock { + fn drop(&mut self) { + let flock = os::flock { + l_start: 0, + l_len: 0, + l_pid: 0, + l_whence: libc::SEEK_SET as libc::c_short, + l_type: os::F_UNLCK, + l_sysid: 0, + }; + unsafe { + libc::fcntl(self.fd, os::F_SETLK, &flock as *os::flock); + libc::close(self.fd); + } + } + } +} + +#[cfg(windows)] +mod imp { + use std::libc; + use std::mem; + use std::os::win32::as_utf16_p; + use std::ptr; + + static LOCKFILE_EXCLUSIVE_LOCK: libc::DWORD = 0x00000002; + + extern "system" { + fn LockFileEx(hFile: libc::HANDLE, + dwFlags: libc::DWORD, + dwReserved: libc::DWORD, + nNumberOfBytesToLockLow: libc::DWORD, + nNumberOfBytesToLockHigh: libc::DWORD, + lpOverlapped: libc::LPOVERLAPPED) -> libc::BOOL; + fn UnlockFileEx(hFile: libc::HANDLE, + dwReserved: libc::DWORD, + nNumberOfBytesToLockLow: libc::DWORD, + nNumberOfBytesToLockHigh: libc::DWORD, + lpOverlapped: libc::LPOVERLAPPED) -> libc::BOOL; + } + + pub struct Lock { + priv handle: libc::HANDLE, + } + + impl Lock { + pub fn new(p: &Path) -> Lock { + let handle = as_utf16_p(p.as_str().unwrap(), |p| unsafe { + libc::CreateFileW(p, libc::GENERIC_READ, 0, ptr::mut_null(), + libc::CREATE_ALWAYS, + libc::FILE_ATTRIBUTE_NORMAL, + ptr::mut_null()) + }); + assert!(handle as uint != libc::INVALID_HANDLE_VALUE as uint); + let mut overlapped: libc::OVERLAPPED = unsafe { mem::init() }; + let ret = unsafe { + LockFileEx(handle, LOCKFILE_EXCLUSIVE_LOCK, 0, 100, 0, + &mut overlapped) + }; + if ret == 0 { + unsafe { libc::CloseHandle(handle); } + fail!("could not lock `{}`", p.display()) + } + Lock { handle: handle } + } + } + + impl Drop for Lock { + fn drop(&mut self) { + let mut overlapped: libc::OVERLAPPED = unsafe { mem::init() }; + unsafe { + UnlockFileEx(self.handle, 0, 100, 0, &mut overlapped); + libc::CloseHandle(self.handle); + } + } + } +} diff --git a/src/librustdoc/html/layout.rs b/src/librustdoc/html/layout.rs index c49df49dc11..317571ebc3f 100644 --- a/src/librustdoc/html/layout.rs +++ b/src/librustdoc/html/layout.rs @@ -37,7 +37,7 @@ pub fn render( - + {favicon, select, none{} other{}} @@ -74,13 +74,6 @@ pub fn render(
- - - - -

Keyboard shortcuts

@@ -111,6 +104,14 @@ pub fn render(

+ + + + + ", diff --git a/src/librustdoc/html/render.rs b/src/librustdoc/html/render.rs index 6ad7b7d9da1..26723482595 100644 --- a/src/librustdoc/html/render.rs +++ b/src/librustdoc/html/render.rs @@ -36,7 +36,7 @@ use std::fmt; use std::local_data; use std::io; -use std::io::{fs, File, BufferedWriter}; +use std::io::{fs, File, BufferedWriter, MemWriter, BufferedReader}; use std::str; use std::vec; use std::vec_ng::Vec; @@ -283,48 +283,75 @@ pub fn run(mut krate: clean::Crate, dst: Path) -> io::IoResult<()> { }; } - // Add all the static files - let mut dst = cx.dst.join(krate.name.as_slice()); - try!(mkdir(&dst)); - try!(write(dst.join("jquery.js"), - include_str!("static/jquery-2.1.0.min.js"))); - try!(write(dst.join("main.js"), include_str!("static/main.js"))); - try!(write(dst.join("main.css"), include_str!("static/main.css"))); - try!(write(dst.join("normalize.css"), - include_str!("static/normalize.css"))); - // Publish the search index - { - dst.push("search-index.js"); - let mut w = BufferedWriter::new(File::create(&dst).unwrap()); - let w = &mut w as &mut Writer; - try!(write!(w, "var searchIndex = [")); + let index = { + let mut w = MemWriter::new(); + try!(write!(&mut w, "searchIndex['{}'] = [", krate.name)); for (i, item) in cache.search_index.iter().enumerate() { if i > 0 { - try!(write!(w, ",")); + try!(write!(&mut w, ",")); } - try!(write!(w, "\\{ty:\"{}\",name:\"{}\",path:\"{}\",desc:{}", - item.ty, item.name, item.path, - item.desc.to_json().to_str())); + try!(write!(&mut w, "\\{ty:\"{}\",name:\"{}\",path:\"{}\",desc:{}", + item.ty, item.name, item.path, + item.desc.to_json().to_str())); match item.parent { Some(id) => { - try!(write!(w, ",parent:'{}'", id)); + try!(write!(&mut w, ",parent:'{}'", id)); } None => {} } - try!(write!(w, "\\}")); + try!(write!(&mut w, "\\}")); } - try!(write!(w, "];")); - try!(write!(w, "var allPaths = \\{")); + try!(write!(&mut w, "];")); + try!(write!(&mut w, "allPaths['{}'] = \\{", krate.name)); for (i, (&id, &(ref fqp, short))) in cache.paths.iter().enumerate() { if i > 0 { - try!(write!(w, ",")); + try!(write!(&mut w, ",")); } - try!(write!(w, "'{}':\\{type:'{}',name:'{}'\\}", - id, short, *fqp.last().unwrap())); + try!(write!(&mut w, "'{}':\\{type:'{}',name:'{}'\\}", + id, short, *fqp.last().unwrap())); } - try!(write!(w, "\\};")); - try!(w.flush()); + try!(write!(&mut w, "\\};")); + + str::from_utf8_owned(w.unwrap()).unwrap() + }; + + // Write out the shared files. Note that these are shared among all rustdoc + // docs placed in the output directory, so this needs to be a synchronized + // operation with respect to all other rustdocs running around. + { + try!(mkdir(&cx.dst)); + let _lock = ::flock::Lock::new(&cx.dst.join(".lock")); + + // Add all the static files. These may already exist, but we just + // overwrite them anyway to make sure that they're fresh and up-to-date. + try!(write(cx.dst.join("jquery.js"), + include_str!("static/jquery-2.1.0.min.js"))); + try!(write(cx.dst.join("main.js"), include_str!("static/main.js"))); + try!(write(cx.dst.join("main.css"), include_str!("static/main.css"))); + try!(write(cx.dst.join("normalize.css"), + include_str!("static/normalize.css"))); + + // Update the search index + let dst = cx.dst.join("search-index.js"); + let mut all_indexes = Vec::new(); + all_indexes.push(index); + if dst.exists() { + for line in BufferedReader::new(File::open(&dst)).lines() { + let line = try!(line); + if !line.starts_with("searchIndex") { continue } + if line.starts_with(format!("searchIndex['{}']", krate.name)) { + continue + } + all_indexes.push(line); + } + } + let mut w = try!(File::create(&dst)); + try!(writeln!(&mut w, r"var searchIndex = \{\}; var allPaths = \{\};")); + for index in all_indexes.iter() { + try!(writeln!(&mut w, "{}", *index)); + } + try!(writeln!(&mut w, "initSearch(searchIndex);")); } // Render all source files (this may turn into a giant no-op) diff --git a/src/librustdoc/html/static/main.js b/src/librustdoc/html/static/main.js index 3056bca68d7..ffdf67e56cf 100644 --- a/src/librustdoc/html/static/main.js +++ b/src/librustdoc/html/static/main.js @@ -9,7 +9,7 @@ // except according to those terms. /*jslint browser: true, es5: true */ -/*globals $: true, searchIndex: true, rootPath: true, allPaths: true */ +/*globals $: true, rootPath: true, allPaths: true */ (function() { "use strict"; @@ -23,7 +23,8 @@ map(function(s) { var pair = s.split("="); params[decodeURIComponent(pair[0])] = - typeof pair[1] === "undefined" ? null : decodeURIComponent(pair[1]); + typeof pair[1] === "undefined" ? + null : decodeURIComponent(pair[1]); }); return params; } @@ -111,8 +112,9 @@ document.location.href = url; }); - function initSearch(searchIndex) { - var currentResults, index, params = getQueryStringParams(); + function initSearch(rawSearchIndex) { + var currentResults, index, searchIndex; + var params = getQueryStringParams(); // Populate search bar with query string search term when provided, // but only if the input bar is empty. This avoid the obnoxious issue @@ -126,7 +128,8 @@ * Executes the query and builds an index of results * @param {[Object]} query [The user query] * @param {[type]} max [The maximum results returned] - * @param {[type]} searchWords [The list of search words to query against] + * @param {[type]} searchWords [The list of search words to query + * against] * @return {[type]} [A search index of results] */ function execQuery(query, max, searchWords) { @@ -148,7 +151,9 @@ // quoted values mean literal search bb = searchWords.length; - if ((val.charAt(0) === "\"" || val.charAt(0) === "'") && val.charAt(val.length - 1) === val.charAt(0)) { + if ((val.charAt(0) === "\"" || val.charAt(0) === "'") && + val.charAt(val.length - 1) === val.charAt(0)) + { val = val.substr(1, val.length - 2); for (aa = 0; aa < bb; aa += 1) { if (searchWords[aa] === val) { @@ -166,7 +171,10 @@ val = val.replace(/\_/g, ""); for (var i = 0; i < split.length; i++) { for (aa = 0; aa < bb; aa += 1) { - if (searchWords[aa].indexOf(split[i]) > -1 || searchWords[aa].indexOf(val) > -1 || searchWords[aa].replace(/_/g, "").indexOf(val) > -1) { + if (searchWords[aa].indexOf(split[i]) > -1 || + searchWords[aa].indexOf(val) > -1 || + searchWords[aa].replace(/_/g, "").indexOf(val) > -1) + { // filter type: ... queries if (!typeFilter || typeFilter === searchIndex[aa].ty) { results.push([aa, searchWords[aa].replace(/_/g, "").indexOf(val)]); @@ -185,6 +193,7 @@ results[aa].push(searchIndex[results[aa][0]].path); results[aa].push(searchIndex[results[aa][0]].name); results[aa].push(searchIndex[results[aa][0]].parent); + results[aa].push(searchIndex[results[aa][0]].crate); } // if there are no results then return to default and fail if (results.length === 0) { @@ -193,7 +202,8 @@ // sort by exact match results.sort(function search_complete_sort0(aaa, bbb) { - if (searchWords[aaa[0]] === valLower && searchWords[bbb[0]] !== valLower) { + if (searchWords[aaa[0]] === valLower && + searchWords[bbb[0]] !== valLower) { return 1; } }); @@ -207,7 +217,8 @@ // second sorting attempt // sort by item name results.sort(function search_complete_sort1(aaa, bbb) { - if (searchWords[aaa[0]].length === searchWords[bbb[0]].length && searchWords[aaa[0]] > searchWords[bbb[0]]) { + if (searchWords[aaa[0]].length === searchWords[bbb[0]].length && + searchWords[aaa[0]] > searchWords[bbb[0]]) { return 1; } }); @@ -223,21 +234,26 @@ // fourth sorting attempt // sort by type results.sort(function search_complete_sort3(aaa, bbb) { - if (searchWords[aaa[0]] === searchWords[bbb[0]] && aaa[2] > bbb[2]) { + if (searchWords[aaa[0]] === searchWords[bbb[0]] && + aaa[2] > bbb[2]) { return 1; } }); // fifth sorting attempt // sort by path results.sort(function search_complete_sort4(aaa, bbb) { - if (searchWords[aaa[0]] === searchWords[bbb[0]] && aaa[2] === bbb[2] && aaa[3] > bbb[3]) { + if (searchWords[aaa[0]] === searchWords[bbb[0]] && + aaa[2] === bbb[2] && aaa[3] > bbb[3]) { return 1; } }); // sixth sorting attempt // remove duplicates, according to the data provided for (aa = results.length - 1; aa > 0; aa -= 1) { - if (searchWords[results[aa][0]] === searchWords[results[aa - 1][0]] && results[aa][2] === results[aa - 1][2] && results[aa][3] === results[aa - 1][3]) { + if (searchWords[results[aa][0]] === searchWords[results[aa - 1][0]] && + results[aa][2] === results[aa - 1][2] && + results[aa][3] === results[aa - 1][3]) + { results[aa][0] = -1; } } @@ -245,7 +261,7 @@ var result = results[i], name = result[4].toLowerCase(), path = result[3].toLowerCase(), - parent = allPaths[result[5]]; + parent = allPaths[result[6]][result[5]]; var valid = validateResult(name, path, split, parent); if (!valid) { @@ -256,11 +272,14 @@ } /** - * Validate performs the following boolean logic. For example: "File::open" will give - * IF A PARENT EXISTS => ("file" && "open") exists in (name || path || parent) - * OR => ("file" && "open") exists in (name || path ) + * Validate performs the following boolean logic. For example: + * "File::open" will give IF A PARENT EXISTS => ("file" && "open") + * exists in (name || path || parent) OR => ("file" && "open") exists in + * (name || path ) + * + * This could be written functionally, but I wanted to minimise + * functions on stack. * - * This could be written functionally, but I wanted to minimise functions on stack. * @param {[string]} name [The name of the result] * @param {[string]} path [The path of the result] * @param {[string]} keys [The keys to be used (["file", "open"])] @@ -273,8 +292,13 @@ //if there is a parent, then validate against parent if (parent !== undefined) { for (var i = 0; i < keys.length; i++) { - // if previous keys are valid and current key is in the path, name or parent - if ((validate) && (name.toLowerCase().indexOf(keys[i]) > -1 || path.toLowerCase().indexOf(keys[i]) > -1 || parent.name.toLowerCase().indexOf(keys[i]) > -1)) { + // if previous keys are valid and current key is in the + // path, name or parent + if ((validate) && + (name.toLowerCase().indexOf(keys[i]) > -1 || + path.toLowerCase().indexOf(keys[i]) > -1 || + parent.name.toLowerCase().indexOf(keys[i]) > -1)) + { validate = true; } else { validate = false; @@ -282,8 +306,12 @@ } } else { for (var i = 0; i < keys.length; i++) { - // if previous keys are valid and current key is in the path, name - if ((validate) && (name.toLowerCase().indexOf(keys[i]) > -1 || path.toLowerCase().indexOf(keys[i]) > -1)) { + // if previous keys are valid and current key is in the + // path, name + if ((validate) && + (name.toLowerCase().indexOf(keys[i]) > -1 || + path.toLowerCase().indexOf(keys[i]) > -1)) + { validate = true; } else { validate = false; @@ -298,7 +326,10 @@ matches = query.match(/^(fn|mod|str(uct)?|enum|trait|t(ype)?d(ef)?)\s*:\s*/i); if (matches) { - type = matches[1].replace(/^td$/, 'typedef').replace(/^str$/, 'struct').replace(/^tdef$/, 'typedef').replace(/^typed$/, 'typedef'); + type = matches[1].replace(/^td$/, 'typedef') + .replace(/^str$/, 'struct') + .replace(/^tdef$/, 'typedef') + .replace(/^typed$/, 'typedef'); query = query.substring(matches[0].length); } @@ -314,7 +345,6 @@ $results.on('click', function() { var dst = $(this).find('a')[0]; - console.log(window.location.pathname, dst.pathname); if (window.location.pathname == dst.pathname) { $('#search').addClass('hidden'); $('#main').removeClass('hidden'); @@ -362,7 +392,8 @@ var output, shown, query = getQuery(); currentResults = query.id; - output = '

Results for ' + query.query + (query.type ? ' (type: ' + query.type + ')' : '') + '

'; + output = '

Results for ' + query.query + + (query.type ? ' (type: ' + query.type + ')' : '') + '

'; output += ''; if (results.length > 0) { @@ -394,7 +425,7 @@ '/index.html" class="' + type + '">' + name + ''; } else if (item.parent !== undefined) { - var myparent = allPaths[item.parent]; + var myparent = allPaths[item.crate][item.parent]; var anchor = '#' + type + '.' + name; output += item.path + '::' + myparent.name + ':: yy[op].toLowerCase())) { - // return 1; - // } - // }); - // }; - showResults(results); } - function buildIndex(searchIndex) { - var len = searchIndex.length, - i = 0, - searchWords = []; + function buildIndex(rawSearchIndex) { + searchIndex = []; + var searchWords = []; + for (var crate in rawSearchIndex) { + if (!rawSearchIndex.hasOwnProperty(crate)) { continue } + var len = rawSearchIndex[crate].length; + var i = 0; - // before any analysis is performed lets gather the search terms to - // search against apart from the rest of the data. This is a quick - // operation that is cached for the life of the page state so that - // all other search operations have access to this cached data for - // faster analysis operations - for (i = 0; i < len; i += 1) { - if (typeof searchIndex[i].name === "string") { - searchWords.push(searchIndex[i].name.toLowerCase()); - } else { - searchWords.push(""); + // before any analysis is performed lets gather the search terms to + // search against apart from the rest of the data. This is a quick + // operation that is cached for the life of the page state so that + // all other search operations have access to this cached data for + // faster analysis operations + for (i = 0; i < len; i += 1) { + rawSearchIndex[crate][i].crate = crate; + searchIndex.push(rawSearchIndex[crate][i]); + if (typeof rawSearchIndex[crate][i].name === "string") { + var word = rawSearchIndex[crate][i].name.toLowerCase(); + searchWords.push(word); + } else { + searchWords.push(""); + } } } - return searchWords; } @@ -567,17 +542,21 @@ clearTimeout(keyUpTimeout); keyUpTimeout = setTimeout(search, 100); }); - // Push and pop states are used to add search results to the browser history. + + // Push and pop states are used to add search results to the browser + // history. if (browserSupportsHistoryApi()) { $(window).on('popstate', function(e) { var params = getQueryStringParams(); - // When browsing back from search results the main page visibility must be reset. + // When browsing back from search results the main page + // visibility must be reset. if (!params.search) { $('#main.content').removeClass('hidden'); $('#search.content').addClass('hidden'); } - // When browsing forward to search results the previous search will be repeated, - // so the currentResults are cleared to ensure the search is successful. + // When browsing forward to search results the previous + // search will be repeated, so the currentResults are + // cleared to ensure the search is successful. currentResults = null; // Synchronize search bar with query string state and // perform the search, but don't empty the bar if there's @@ -585,19 +564,46 @@ if (params.search !== undefined) { $('.search-input').val(params.search); } - // Some browsers fire 'onpopstate' for every page load (Chrome), while others fire the - // event only when actually popping a state (Firefox), which is why search() is called - // both here and at the end of the startSearch() function. + // Some browsers fire 'onpopstate' for every page load + // (Chrome), while others fire the event only when actually + // popping a state (Firefox), which is why search() is + // called both here and at the end of the startSearch() + // function. search(); }); } search(); } - index = buildIndex(searchIndex); + index = buildIndex(rawSearchIndex); startSearch(); + + // Draw a convenient sidebar of known crates if we have a listing + if (rootPath == '../') { + console.log('here'); + var sidebar = $('.sidebar'); + var div = $('
').attr('class', 'block crate'); + div.append($('

').text('Crates')); + + var crates = []; + for (var crate in rawSearchIndex) { + if (!rawSearchIndex.hasOwnProperty(crate)) { continue } + crates.push(crate); + } + crates.sort(); + for (var i = 0; i < crates.length; i++) { + var klass = 'crate'; + if (crates[i] == window.currentCrate) { + klass += ' current'; + } + div.append($('', {'href': '../' + crates[i] + '/index.html', + 'class': klass}).text(crates[i])); + div.append($('
')); + } + sidebar.append(div); + } } - initSearch(searchIndex); - + window.initSearch = initSearch; }()); + diff --git a/src/librustdoc/lib.rs b/src/librustdoc/lib.rs index e64292285d0..6dc3c2073f1 100644 --- a/src/librustdoc/lib.rs +++ b/src/librustdoc/lib.rs @@ -52,6 +52,7 @@ pub mod passes; pub mod plugins; pub mod visit_ast; pub mod test; +mod flock; pub static SCHEMA_VERSION: &'static str = "0.8.1"; diff --git a/src/libstd/libc.rs b/src/libstd/libc.rs index 0fb7a5f8503..42221f07449 100644 --- a/src/libstd/libc.rs +++ b/src/libstd/libc.rs @@ -3643,7 +3643,7 @@ pub mod funcs { pub fn open(path: *c_char, oflag: c_int, mode: c_int) -> c_int; pub fn creat(path: *c_char, mode: mode_t) -> c_int; - pub fn fcntl(fd: c_int, cmd: c_int) -> c_int; + pub fn fcntl(fd: c_int, cmd: c_int, ...) -> c_int; } }